Barmaja-PyArduino 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. Barmaja_PyArduino/BARMAJA_PyArduino.ico +0 -0
  2. Barmaja_PyArduino/__init__.py +528 -0
  3. Barmaja_PyArduino/__init__.pyi +331 -0
  4. Barmaja_PyArduino/__main__.py +2 -0
  5. Barmaja_PyArduino/adapters/__init__.py +35 -0
  6. Barmaja_PyArduino/api_registry.py +459 -0
  7. Barmaja_PyArduino/backend/__init__.py +3 -0
  8. Barmaja_PyArduino/backend/uno_cpp.py +1680 -0
  9. Barmaja_PyArduino/board.py +80 -0
  10. Barmaja_PyArduino/cli.py +223 -0
  11. Barmaja_PyArduino/compiler.py +1721 -0
  12. Barmaja_PyArduino/console.py +217 -0
  13. Barmaja_PyArduino/device_packs.py +367 -0
  14. Barmaja_PyArduino/device_packs_builtin/Button/arduino/Barmaja_Button/library.properties +9 -0
  15. Barmaja_PyArduino/device_packs_builtin/Button/arduino/Barmaja_Button/src/Barmaja_Button.h +18 -0
  16. Barmaja_PyArduino/device_packs_builtin/Button/example.py +10 -0
  17. Barmaja_PyArduino/device_packs_builtin/Button/pack.json +40 -0
  18. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/arduino/Barmaja_ClockDS3231/library.properties +9 -0
  19. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/arduino/Barmaja_ClockDS3231/src/Barmaja_ClockDS3231.h +20 -0
  20. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/example.py +8 -0
  21. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/pack.json +136 -0
  22. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/arduino/Barmaja_DistanceVL53L0X/library.properties +9 -0
  23. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/arduino/Barmaja_DistanceVL53L0X/src/Barmaja_DistanceVL53L0X.h +12 -0
  24. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/example.py +8 -0
  25. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/pack.json +48 -0
  26. Barmaja_PyArduino/device_packs_builtin/LCDParallel/arduino/Barmaja_LCDParallel/library.properties +9 -0
  27. Barmaja_PyArduino/device_packs_builtin/LCDParallel/arduino/Barmaja_LCDParallel/src/Barmaja_LCDParallel.h +17 -0
  28. Barmaja_PyArduino/device_packs_builtin/LCDParallel/example.py +10 -0
  29. Barmaja_PyArduino/device_packs_builtin/LCDParallel/pack.json +137 -0
  30. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/arduino/Barmaja_LoadCellHX711/library.properties +9 -0
  31. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/arduino/Barmaja_LoadCellHX711/src/Barmaja_LoadCellHX711.h +16 -0
  32. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/example.py +11 -0
  33. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/pack.json +74 -0
  34. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/arduino/Barmaja_NeoPixelStrip/library.properties +9 -0
  35. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/arduino/Barmaja_NeoPixelStrip/src/Barmaja_NeoPixelStrip.h +15 -0
  36. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/example.py +13 -0
  37. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/pack.json +114 -0
  38. Barmaja_PyArduino/device_packs_builtin/OLEDText/arduino/Barmaja_OLEDText/library.properties +9 -0
  39. Barmaja_PyArduino/device_packs_builtin/OLEDText/arduino/Barmaja_OLEDText/src/Barmaja_OLEDText.h +21 -0
  40. Barmaja_PyArduino/device_packs_builtin/OLEDText/example.py +9 -0
  41. Barmaja_PyArduino/device_packs_builtin/OLEDText/pack.json +103 -0
  42. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/arduino/Barmaja_PressureBMP280/library.properties +9 -0
  43. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/arduino/Barmaja_PressureBMP280/src/Barmaja_PressureBMP280.h +16 -0
  44. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/example.py +8 -0
  45. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/pack.json +92 -0
  46. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/arduino/Barmaja_RotaryEncoder/library.properties +9 -0
  47. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/arduino/Barmaja_RotaryEncoder/src/Barmaja_RotaryEncoder.h +18 -0
  48. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/example.py +10 -0
  49. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/pack.json +44 -0
  50. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/arduino/Barmaja_ServoDriverPCA9685/library.properties +9 -0
  51. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/arduino/Barmaja_ServoDriverPCA9685/src/Barmaja_ServoDriverPCA9685.h +15 -0
  52. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/example.py +10 -0
  53. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/pack.json +117 -0
  54. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/arduino/Barmaja_ShiftRegister595/library.properties +9 -0
  55. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/arduino/Barmaja_ShiftRegister595/src/Barmaja_ShiftRegister595.h +14 -0
  56. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/example.py +9 -0
  57. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/pack.json +83 -0
  58. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/arduino/Barmaja_TemperatureDS18B20/library.properties +9 -0
  59. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/arduino/Barmaja_TemperatureDS18B20/src/Barmaja_TemperatureDS18B20.h +16 -0
  60. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/example.py +8 -0
  61. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/pack.json +63 -0
  62. Barmaja_PyArduino/doctor.py +406 -0
  63. Barmaja_PyArduino/environment.py +595 -0
  64. Barmaja_PyArduino/errors.py +166 -0
  65. Barmaja_PyArduino/flow.py +754 -0
  66. Barmaja_PyArduino/frontend.py +611 -0
  67. Barmaja_PyArduino/installer.py +151 -0
  68. Barmaja_PyArduino/ir.py +660 -0
  69. Barmaja_PyArduino/library_adder.py +440 -0
  70. Barmaja_PyArduino/live.py +859 -0
  71. Barmaja_PyArduino/lowering.py +596 -0
  72. Barmaja_PyArduino/numeric.py +748 -0
  73. Barmaja_PyArduino/pack_builder.py +544 -0
  74. Barmaja_PyArduino/py.typed +0 -0
  75. Barmaja_PyArduino/serial_monitor.py +172 -0
  76. Barmaja_PyArduino/source.py +184 -0
  77. Barmaja_PyArduino/stubgen.py +148 -0
  78. Barmaja_PyArduino/symbols.py +646 -0
  79. Barmaja_PyArduino/target.py +1631 -0
  80. Barmaja_PyArduino/toolchain.py +506 -0
  81. Barmaja_PyArduino/typesys.py +917 -0
  82. Barmaja_PyArduino/workflow.py +213 -0
  83. barmaja_pyarduino-2.0.0.dist-info/METADATA +39 -0
  84. barmaja_pyarduino-2.0.0.dist-info/RECORD +87 -0
  85. barmaja_pyarduino-2.0.0.dist-info/WHEEL +5 -0
  86. barmaja_pyarduino-2.0.0.dist-info/licenses/LICENSE.txt +7 -0
  87. barmaja_pyarduino-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1680 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass, fields, is_dataclass
5
+ from typing import Any
6
+
7
+ from ..errors import InternalCompilerError
8
+ from ..flow import StorageClass
9
+ from ..ir import (
10
+ IRAugStore, IRBinary, IRBinaryOp, IRBool, IRBoolOp, IRBreak, IRCall, IRCallKind,
11
+ IRCompareChain, IRCompareOp, IRConditional, IRConstant, IRContinue, IREffect,
12
+ IRExpr, IRExprStmt, IRFixedListLoop, IRFormatString, IRFormattedValue, IRFunction,
13
+ IRIf, IRIndex, IRIndexStore, IRListLiteral, IRLoad, IRLoopControlTarget, IRModule,
14
+ IRPrint, IRRangeLoop, IRReturn, IRStmt, IRStore, IRSymbolRef,
15
+ IRTempRef, IRUnary, IRUnaryOp, IRWhile, validate_ir,
16
+ )
17
+ from ..source import SourceSpan
18
+ from ..symbols import SymbolKind
19
+ from ..target import TargetDiagnostic, TargetPlan
20
+ from ..typesys import TypeKind
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class SourceMapEntry:
25
+ cpp_line: int
26
+ source_name: str
27
+ span: SourceSpan
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class GeneratedSketch:
32
+ cpp: str
33
+ source_map: tuple[SourceMapEntry, ...]
34
+ dependency_ids: tuple[str, ...]
35
+ resolved_serial_baud: int | None
36
+ diagnostics: tuple[TargetDiagnostic, ...]
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class _OutputPart:
41
+ literal: str | None = None
42
+ expression: str | None = None
43
+ type_kind: TypeKind | None = None
44
+ precision: int | None = None
45
+
46
+
47
+ # _bj_numeric_fail() halts the Uno. Its loop MUST contain a side effect and stay out of
48
+ # line: with the pinned avr-gcc 7.3.0 + -Os -flto, an empty `for (;;) {}` let GCC's
49
+ # control-dependent DCE delete the non-failing path after an inlined check (WA-26).
50
+ _NUMERIC_HELPER_BLOB ='static void __attribute__((noinline, noreturn)) _bj_numeric_fail() {\n for (;;) { __asm__ __volatile__(""); }\n}\nstatic int32_t _bj_list_index(int32_t index, int32_t length) {\n if (index < 0 || index >= length) _bj_numeric_fail();\n return index;\n}\nstatic int32_t _bj_i32_add(int32_t a, int32_t b) {\n int64_t r = (int64_t)a + (int64_t)b;\n if (r < (-2147483647LL - 1LL) || r > 2147483647LL) _bj_numeric_fail();\n return (int32_t)r;\n}\nstatic int32_t _bj_i32_sub(int32_t a, int32_t b) {\n int64_t r = (int64_t)a - (int64_t)b;\n if (r < (-2147483647LL - 1LL) || r > 2147483647LL) _bj_numeric_fail();\n return (int32_t)r;\n}\nstatic int32_t _bj_i32_mul(int32_t a, int32_t b) {\n int64_t r = (int64_t)a * (int64_t)b;\n if (r < (-2147483647LL - 1LL) || r > 2147483647LL) _bj_numeric_fail();\n return (int32_t)r;\n}\nstatic int32_t _bj_i32_neg(int32_t value) {\n if (value == (-2147483647L - 1L)) _bj_numeric_fail();\n return -value;\n}\nstatic float _bj_true_div_f32(float a, float b) {\n if (b == 0.0f) _bj_numeric_fail();\n return a / b;\n}\nstatic int32_t _bj_mod_i32(int32_t a, int32_t b) {\n if (b == 0) _bj_numeric_fail();\n if (a == (-2147483647L - 1L) && b == -1) return 0;\n int32_t r = a % b;\n if (r != 0 && ((r < 0) != (b < 0))) r += b;\n return r;\n}\nstatic float _bj_mod_f32(float a, float b) {\n if (isnan(a) || isnan(b) || isinf(a) || isinf(b) || b == 0.0f) _bj_numeric_fail();\n float r = fmodf(a, b);\n if (r != 0.0f && ((r < 0.0f) != (b < 0.0f))) r += b;\n if (r == 0.0f) r = copysignf(0.0f, b);\n return r;\n}\nstatic int32_t _bj_digital_pin(int32_t pin) {\n if (pin < 0 || pin > 19) _bj_numeric_fail();\n return pin;\n}\nstatic int32_t _bj_pwm_pin(int32_t pin) {\n if (!(pin == 3 || pin == 5 || pin == 6 || pin == 9 || pin == 10 || pin == 11)) _bj_numeric_fail();\n return pin;\n}\nstatic int32_t _bj_analog_pin(int32_t pin) {\n if (!((pin >= 0 && pin <= 5) || (pin >= 14 && pin <= 19))) _bj_numeric_fail();\n return pin;\n}\nstatic int32_t _bj_servo_angle(int32_t value) {\n if (value < 0 || value > 180) _bj_numeric_fail();\n return value;\n}\nstatic int32_t _bj_servo_pulse(int32_t value) {\n if (value < 0 || value > 3000) _bj_numeric_fail();\n return value;\n}\nstatic void _bj_servo_attach_bounds(int32_t min_us, int32_t max_us) {\n int64_t dmin = 544LL - (int64_t)min_us; if (dmin < 0) dmin = -dmin;\n int64_t dmax = 2400LL - (int64_t)max_us; if (dmax < 0) dmax = -dmax;\n if (min_us <= 0 || min_us >= max_us || dmin / 4LL >= 128LL || dmax / 4LL >= 128LL) _bj_numeric_fail();\n}\nstatic int32_t _bj_pwm_value(int32_t value) {\n if (value < 0 || value > 255) _bj_numeric_fail();\n return value;\n}\nstatic int32_t _bj_byte_value(int32_t value) {\n if (value < 0 || value > 255) _bj_numeric_fail();\n return value;\n}\nstatic int32_t _bj_digital_level(int32_t value) {\n if (value < 0 || value > 1) _bj_numeric_fail();\n return value;\n}\nstatic int32_t _bj_pin_mode(int32_t value) {\n if (!(value == 0 || value == 1 || value == 2)) _bj_numeric_fail();\n return value;\n}\nstatic int32_t _bj_i2c_address(int32_t value) {\n if (value < 0 || value > 127) _bj_numeric_fail();\n return value;\n}\nstatic int32_t _bj_arduino_format(int32_t value) {\n if (value < 0 || value > 255) _bj_numeric_fail();\n return value;\n}\nstatic int32_t _bj_wire_count(int32_t value) {\n if (value < 0 || value > 255) _bj_numeric_fail();\n return value;\n}\nstatic int32_t _bj_nonnegative_int(int32_t value) {\n if (value < 0) _bj_numeric_fail();\n return value;\n}\nstatic int32_t _bj_lcd_dimension(int32_t value) {\n if (value < 1 || value > 255) _bj_numeric_fail();\n return value;\n}\nstatic int32_t _bj_lcd_charsize(int32_t value) {\n if (!(value == 0 || value == 4)) _bj_numeric_fail();\n return value;\n}\nstatic int32_t _bj_lcd_cursor_column(int32_t value) {\n if (value < 0 || value > 255) _bj_numeric_fail();\n return value;\n}\nstatic int32_t _bj_lcd_cursor_row(int32_t value) {\n if (value < 0 || value > 3) _bj_numeric_fail();\n return value;\n}\nstatic int32_t _bj_positive_int(int32_t value) {\n if (value <= 0) _bj_numeric_fail();\n return value;\n}\nstatic uint32_t _bj_time_threshold(int32_t value) {\n if (value < 0) _bj_numeric_fail();\n return (uint32_t)value;\n}\nstatic int32_t _bj_float_to_i32(float value) {\n if (isnan(value) || isinf(value) || value < -2147483648.0f || value >= 2147483648.0f) _bj_numeric_fail();\n return (int32_t)value;\n}\nstatic float _bj_round_even_f32(float value) {\n float base = floorf(value);\n float frac = value - base;\n if (frac > 0.5f) return base + 1.0f;\n if (frac < 0.5f) return base;\n return (fmodf(fabsf(base), 2.0f) == 0.0f) ? base : (base + 1.0f);\n}\nstatic int32_t _bj_round_i32(float value) {\n if (isnan(value) || isinf(value) || value < -2147483648.0f || value >= 2147483648.0f) _bj_numeric_fail();\n float rounded = _bj_round_even_f32(value);\n if (rounded < -2147483648.0f || rounded >= 2147483648.0f) _bj_numeric_fail();\n return (int32_t)rounded;\n}\nstatic __attribute__((noinline)) uint64_t _bj_round_ratio_even_u64(uint64_t num, uint64_t den) {\n uint64_t q = num / den;\n uint64_t r = num % den;\n uint64_t twice = r * 2ULL;\n if (twice > den || (twice == den && (q & 1ULL))) ++q;\n return q;\n}\nstatic __attribute__((noinline)) uint64_t _bj_round_pow2_even_u64(uint64_t num, uint8_t shift) {\n if (shift == 0) return num;\n if (shift >= 64) return 0;\n uint64_t q = num >> shift;\n uint64_t mask = (1ULL << shift) - 1ULL;\n uint64_t r = num & mask;\n uint64_t half = 1ULL << (shift - 1);\n if (r > half || (r == half && (q & 1ULL))) ++q;\n return q;\n}\nstatic __attribute__((noinline)) float _bj_ratio_to_f32(bool negative, uint64_t num, uint64_t den) {\n union { uint32_t u; float f; } out;\n if (num == 0) { out.u = negative ? 0x80000000UL : 0UL; return out.f; }\n int16_t exponent = 0;\n if (num >= den) {\n uint64_t scaled_den = den;\n while (num >= (scaled_den << 1)) { scaled_den <<= 1; ++exponent; }\n } else {\n uint64_t scaled_num = num;\n while (scaled_num < den) { scaled_num <<= 1; --exponent; }\n }\n int16_t shift = 23 - exponent;\n uint64_t scaled_num = num;\n uint64_t scaled_den = den;\n if (shift >= 0) scaled_num <<= shift; else scaled_den <<= -shift;\n uint64_t significand = _bj_round_ratio_even_u64(scaled_num, scaled_den);\n if (significand == (1ULL << 24)) { significand >>= 1; ++exponent; }\n uint32_t exponent_bits = (uint32_t)(exponent + 127);\n if (exponent_bits == 0 || exponent_bits >= 255) _bj_numeric_fail();\n out.u = (negative ? 0x80000000UL : 0UL) | (exponent_bits << 23) | ((uint32_t)significand & 0x7FFFFFUL);\n return out.f;\n}\nstatic float _bj_round_f32_digits(float value, int8_t ndigits) {\n if (isnan(value) || isinf(value) || value == 0.0f) return value;\n union { float f; uint32_t u; } bits; bits.f = value;\n bool negative = (bits.u & 0x80000000UL) != 0;\n uint32_t exp_bits = (bits.u >> 23) & 0xFFUL;\n uint32_t fraction = bits.u & 0x7FFFFFUL;\n uint64_t mantissa; int16_t exponent2; int16_t unbiased;\n if (exp_bits == 0) { mantissa = fraction; exponent2 = -149; unbiased = -126; }\n else { mantissa = (1UL << 23) | fraction; unbiased = (int16_t)exp_bits - 127; exponent2 = unbiased - 23; }\n static const uint32_t p5[7] = {1UL, 5UL, 25UL, 125UL, 625UL, 3125UL, 15625UL};\n static const uint32_t p10[7] = {1UL, 10UL, 100UL, 1000UL, 10000UL, 100000UL, 1000000UL};\n if (ndigits >= 0) {\n if (unbiased >= 23) return value;\n uint8_t n = (uint8_t)ndigits;\n uint64_t mag = mantissa * (uint64_t)p5[n];\n int16_t shift2 = exponent2 + n;\n uint64_t rounded;\n if (shift2 >= 0) rounded = mag << shift2;\n else rounded = _bj_round_pow2_even_u64(mag, (uint8_t)(-shift2));\n return _bj_ratio_to_f32(negative, rounded, p10[n]);\n }\n uint8_t n = (uint8_t)(-ndigits);\n if (unbiased >= 43) return value;\n int16_t shift2 = exponent2 - n;\n uint64_t rounded;\n if (shift2 >= 0) {\n uint64_t numerator = mantissa << shift2;\n rounded = _bj_round_ratio_even_u64(numerator, p5[n]);\n } else {\n uint8_t down = (uint8_t)(-shift2);\n if (down >= 24) rounded = 0;\n else rounded = _bj_round_ratio_even_u64(mantissa, ((uint64_t)p5[n]) << down);\n }\n return _bj_ratio_to_f32(negative, rounded * (uint64_t)p10[n], 1UL);\n}\nstatic int32_t _bj_round_i32_digits(int32_t value, int8_t ndigits) {\n if (ndigits >= 0) return value;\n static const int32_t p10[7] = {1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L};\n int64_t scale = p10[-ndigits];\n int64_t q = ((int64_t)value) / scale;\n int64_t r = ((int64_t)value) % scale;\n int64_t ar = r < 0 ? -r : r;\n int64_t twice = ar * 2LL;\n if (twice > scale || (twice == scale && (q % 2LL) != 0LL)) q += value < 0 ? -1LL : 1LL;\n int64_t result = q * scale;\n if (result < (-2147483647LL - 1LL) || result > 2147483647LL) _bj_numeric_fail();\n return (int32_t)result;\n}\nstatic int32_t _bj_i32_abs(int32_t value) {\n if (value == (-2147483647L - 1L)) _bj_numeric_fail();\n return value < 0 ? -value : value;\n}\nstatic bool _bj_str_eq(const char* a, const char* b) {\n while (*a && *b && *a == *b) { ++a; ++b; }\n return *a == *b;\n}\n'
51
+ _OUTPUT_HELPER_PREAMBLE = 'typedef struct { uint16_t limb[12]; } _bj_big192;'
52
+ _OUTPUT_HELPER_BLOB = r'''static __attribute__((noinline)) uint64_t _bj_pow5_u64(uint8_t n) {
53
+ uint64_t r = 1ULL;
54
+ while (n--) r *= 5ULL;
55
+ return r;
56
+ }
57
+ static __attribute__((noinline)) float _bj_f32_scale2(float value, int16_t shift) {
58
+ // Exact multiplication by 2^shift for normal results (replaces ldexp).
59
+ union { float f; uint32_t u; } bits; bits.f = value;
60
+ int16_t e = (int16_t)((bits.u >> 23) & 0xFFUL) + shift;
61
+ if (e <= 0 || e >= 255) return value * 0.0f + (shift > 0 ? INFINITY : 0.0f);
62
+ bits.u = (bits.u & 0x807FFFFFUL) | ((uint32_t)e << 23);
63
+ return bits.f;
64
+ }
65
+ static __attribute__((noinline)) bool _bj_f32_ge_pow10(uint64_t mantissa, int16_t exponent2, int16_t k) {
66
+ // Exact test of mantissa * 2^exponent2 >= 10^k for -9 <= k <= 16.
67
+ if (k >= 0) {
68
+ if (k > 16) return false;
69
+ uint64_t p5 = _bj_pow5_u64((uint8_t)k);
70
+ int16_t t = exponent2 - k; /* mantissa * 2^t >= 5^k */
71
+ if (t >= 0) return t >= 40 || (mantissa << t) >= p5;
72
+ if (-t > 25) return false;
73
+ return mantissa >= (p5 << (-t));
74
+ }
75
+ int16_t u = -k;
76
+ if (u > 16) return true;
77
+ uint64_t lhs = mantissa * _bj_pow5_u64((uint8_t)u); /* mantissa * 5^u * 2^(exponent2+u) >= 1 */
78
+ int16_t t = exponent2 + u;
79
+ if (t >= 0) return true;
80
+ if (-t >= 63) return false;
81
+ return lhs >= (1ULL << (-t));
82
+ }
83
+ static bool __attribute__((noinline)) _bj_f32_shortest(float value, char* digits, uint8_t* count, int16_t* exp10) {
84
+ // Exact shortest round-trip decimal for float32 in 1e-7 <= |value| < 1e16.
85
+ union { float f; uint32_t u; } bits; bits.f = value;
86
+ bool negative = (bits.u & 0x80000000UL) != 0;
87
+ uint32_t exp_bits = (bits.u >> 23) & 0xFFUL;
88
+ if (exp_bits == 0 || exp_bits == 0xFFUL) return false;
89
+ float magnitude = fabsf(value);
90
+ if (!(magnitude >= 1e-7f && magnitude < 1e16f)) return false;
91
+ uint64_t mantissa = (1ULL << 23) | (bits.u & 0x7FFFFFUL);
92
+ int16_t exponent2 = (int16_t)exp_bits - 150;
93
+ // Exact decimal exponent: 10^k <= |value| < 10^(k+1); integer log10(2) estimate then correction.
94
+ int16_t k_estimate = (int16_t)(((int32_t)(exponent2 + 23) * 1233L) >> 12);
95
+ for (uint8_t fix = 0; fix < 6; ++fix) {
96
+ if (!_bj_f32_ge_pow10(mantissa, exponent2, k_estimate)) { --k_estimate; continue; }
97
+ if (_bj_f32_ge_pow10(mantissa, exponent2, (int16_t)(k_estimate + 1))) { ++k_estimate; continue; }
98
+ break;
99
+ }
100
+ uint32_t low = 1UL; /* 10^(precision-1) */
101
+ for (uint8_t precision = 1; precision <= 9; ++precision, low *= 10UL) {
102
+ uint64_t high = (uint64_t)low * 10ULL; /* 10^precision */
103
+ int16_t k = k_estimate;
104
+ uint64_t n = 0;
105
+ bool ok = false;
106
+ for (uint8_t attempt = 0; attempt < 4; ++attempt) {
107
+ int16_t s = (int16_t)precision - 1 - k;
108
+ if (s > 16 || s < -16) break;
109
+ if (s >= 0) {
110
+ uint64_t scaled = mantissa * _bj_pow5_u64((uint8_t)s);
111
+ int16_t t = exponent2 + s;
112
+ if (t >= 0) { if (t > 30) break; n = scaled << t; }
113
+ else n = _bj_round_pow2_even_u64(scaled, (uint8_t)((-t) > 64 ? 64 : (-t)));
114
+ } else {
115
+ uint8_t u = (uint8_t)(-s);
116
+ int16_t t = exponent2 - u;
117
+ if (t >= 0) { if (t > 30) break; n = _bj_round_ratio_even_u64(mantissa << t, _bj_pow5_u64(u)); }
118
+ else { int16_t shift = -t; if (shift > 25) break; n = _bj_round_ratio_even_u64(mantissa, _bj_pow5_u64(u) << shift); }
119
+ }
120
+ if (n >= high) { ++k; continue; }
121
+ if (n < low) { --k; continue; }
122
+ ok = true;
123
+ break;
124
+ }
125
+ if (!ok) continue;
126
+ int16_t s = (int16_t)precision - 1 - k;
127
+ float back = (s >= 0)
128
+ ? _bj_f32_scale2(_bj_ratio_to_f32(negative, n, _bj_pow5_u64((uint8_t)s)), (int16_t)(-s))
129
+ : _bj_f32_scale2(_bj_ratio_to_f32(negative, n * _bj_pow5_u64((uint8_t)(-s)), 1ULL), (int16_t)(-s));
130
+ if (back == value) {
131
+ uint32_t x = (uint32_t)n;
132
+ for (int8_t i = (int8_t)precision - 1; i >= 0; --i) { digits[i] = (char)('0' + (x % 10UL)); x /= 10UL; }
133
+ *count = precision;
134
+ *exp10 = k;
135
+ return true;
136
+ }
137
+ }
138
+ return false;
139
+ }
140
+ // Full-range exact fallback for float32. The AVR libc decimal conversion
141
+ // routines are not correctly-rounded enough for shortest-repr decisions at
142
+ // the exponent extremes, so this path uses integer arithmetic and IEEE-754 interval
143
+ // boundaries. 12 base-2^16 limbs (192 bits) are sufficient for every
144
+ // binary32 value scaled by the powers of five needed for 1..9 decimal digits.
145
+ static void _bj_big_zero(_bj_big192* a) {
146
+ for (uint8_t i = 0; i < 12; ++i) a->limb[i] = 0;
147
+ }
148
+ static void _bj_big_from_u32(_bj_big192* a, uint32_t value) {
149
+ _bj_big_zero(a);
150
+ a->limb[0] = (uint16_t)(value & 0xFFFFUL);
151
+ a->limb[1] = (uint16_t)(value >> 16);
152
+ }
153
+ static void _bj_big_copy(_bj_big192* dst, const _bj_big192* src) {
154
+ for (uint8_t i = 0; i < 12; ++i) dst->limb[i] = src->limb[i];
155
+ }
156
+ static void _bj_big_mul5(_bj_big192* a, uint8_t count) {
157
+ while (count--) {
158
+ uint32_t carry = 0;
159
+ for (uint8_t i = 0; i < 12; ++i) {
160
+ uint32_t v = (uint32_t)a->limb[i] * 5UL + carry;
161
+ a->limb[i] = (uint16_t)(v & 0xFFFFUL);
162
+ carry = v >> 16;
163
+ }
164
+ }
165
+ }
166
+ static void _bj_big_shl1(_bj_big192* a) {
167
+ uint16_t carry = 0;
168
+ for (uint8_t i = 0; i < 12; ++i) {
169
+ uint16_t next = (uint16_t)(a->limb[i] >> 15);
170
+ a->limb[i] = (uint16_t)((a->limb[i] << 1) | carry);
171
+ carry = next;
172
+ }
173
+ }
174
+ static void _bj_big_shr1(_bj_big192* a) {
175
+ uint16_t carry = 0;
176
+ for (int8_t i = 11; i >= 0; --i) {
177
+ uint16_t next = (uint16_t)(a->limb[i] & 1U);
178
+ a->limb[i] = (uint16_t)((a->limb[i] >> 1) | (uint16_t)(carry << 15));
179
+ carry = next;
180
+ }
181
+ }
182
+ static void _bj_big_shl(_bj_big192* a, uint16_t bits) {
183
+ while (bits--) _bj_big_shl1(a);
184
+ }
185
+ static void _bj_big_shr(_bj_big192* a, uint16_t bits) {
186
+ while (bits--) _bj_big_shr1(a);
187
+ }
188
+ static int8_t _bj_big_cmp(const _bj_big192* a, const _bj_big192* b) {
189
+ for (int8_t i = 11; i >= 0; --i) {
190
+ if (a->limb[i] < b->limb[i]) return -1;
191
+ if (a->limb[i] > b->limb[i]) return 1;
192
+ }
193
+ return 0;
194
+ }
195
+ static void _bj_big_sub(_bj_big192* a, const _bj_big192* b) {
196
+ uint32_t borrow = 0;
197
+ for (uint8_t i = 0; i < 12; ++i) {
198
+ uint32_t av = a->limb[i];
199
+ uint32_t bv = (uint32_t)b->limb[i] + borrow;
200
+ if (av >= bv) { a->limb[i] = (uint16_t)(av - bv); borrow = 0; }
201
+ else { a->limb[i] = (uint16_t)(0x10000UL + av - bv); borrow = 1; }
202
+ }
203
+ }
204
+ static uint16_t _bj_big_bitlen(const _bj_big192* a) {
205
+ for (int8_t i = 11; i >= 0; --i) {
206
+ uint16_t x = a->limb[i];
207
+ if (x) {
208
+ uint8_t n = 0;
209
+ while (x) { x >>= 1; ++n; }
210
+ return (uint16_t)((uint16_t)i * 16U + n);
211
+ }
212
+ }
213
+ return 0;
214
+ }
215
+ static bool _bj_big_to_u32(const _bj_big192* a, uint32_t* out) {
216
+ for (uint8_t i = 2; i < 12; ++i) if (a->limb[i]) return false;
217
+ *out = (uint32_t)a->limb[0] | ((uint32_t)a->limb[1] << 16);
218
+ return true;
219
+ }
220
+ static bool _bj_big_bit(const _bj_big192* a, uint16_t bit) {
221
+ if (bit >= 192) return false;
222
+ return ((a->limb[bit >> 4] >> (bit & 15)) & 1U) != 0;
223
+ }
224
+ static bool _bj_big_any_low_bits(const _bj_big192* a, uint16_t count) {
225
+ if (count > 192) count = 192;
226
+ for (uint16_t bit = 0; bit < count; ++bit) if (_bj_big_bit(a, bit)) return true;
227
+ return false;
228
+ }
229
+ static bool _bj_big_round_pow2_u32(const _bj_big192* input, uint16_t shift, uint32_t* out) {
230
+ if (shift >= 193) { *out = 0; return true; }
231
+ bool half = shift ? _bj_big_bit(input, (uint16_t)(shift - 1)) : false;
232
+ bool sticky = shift > 1 ? _bj_big_any_low_bits(input, (uint16_t)(shift - 1)) : false;
233
+ _bj_big192 q; _bj_big_copy(&q, input);
234
+ _bj_big_shr(&q, shift);
235
+ uint32_t value;
236
+ if (!_bj_big_to_u32(&q, &value)) return false;
237
+ if (half && (sticky || (value & 1UL))) ++value;
238
+ *out = value;
239
+ return true;
240
+ }
241
+ static bool _bj_big_div_round_u32(const _bj_big192* numerator, const _bj_big192* denominator, uint32_t* out) {
242
+ uint16_t den_bits = _bj_big_bitlen(denominator);
243
+ if (den_bits == 0) return false;
244
+ _bj_big192 rem; _bj_big_copy(&rem, numerator);
245
+ uint32_t q = 0;
246
+ uint16_t num_bits = _bj_big_bitlen(&rem);
247
+ if (num_bits >= den_bits) {
248
+ uint16_t shift = (uint16_t)(num_bits - den_bits);
249
+ _bj_big192 d; _bj_big_copy(&d, denominator); _bj_big_shl(&d, shift);
250
+ for (int16_t i = (int16_t)shift; i >= 0; --i) {
251
+ if (_bj_big_cmp(&rem, &d) >= 0) {
252
+ _bj_big_sub(&rem, &d);
253
+ if (i >= 32) return false;
254
+ q |= (1UL << i);
255
+ }
256
+ _bj_big_shr1(&d);
257
+ }
258
+ }
259
+ _bj_big192 twice; _bj_big_copy(&twice, &rem); _bj_big_shl1(&twice);
260
+ int8_t cmp = _bj_big_cmp(&twice, denominator);
261
+ if (cmp > 0 || (cmp == 0 && (q & 1UL))) ++q;
262
+ *out = q;
263
+ return true;
264
+ }
265
+ static int8_t _bj_dec_vs_binary(uint32_t n, int16_t scale10, uint32_t mantissa, int16_t exponent2) {
266
+ // Compare n * 10^(-scale10) with mantissa * 2^exponent2 exactly.
267
+ _bj_big192 a, b;
268
+ _bj_big_from_u32(&a, n);
269
+ _bj_big_from_u32(&b, mantissa);
270
+ if (scale10 >= 0) {
271
+ _bj_big_mul5(&b, (uint8_t)scale10);
272
+ int16_t q = (int16_t)(exponent2 + scale10);
273
+ if (q >= 0) _bj_big_shl(&b, (uint16_t)q);
274
+ else _bj_big_shl(&a, (uint16_t)(-q));
275
+ } else {
276
+ uint8_t t = (uint8_t)(-scale10);
277
+ _bj_big_mul5(&a, t);
278
+ int16_t q = (int16_t)(exponent2 - t);
279
+ if (q >= 0) _bj_big_shl(&b, (uint16_t)q);
280
+ else _bj_big_shl(&a, (uint16_t)(-q));
281
+ }
282
+ return _bj_big_cmp(&a, &b);
283
+ }
284
+ static bool _bj_scaled_decimal_round(uint32_t mantissa, int16_t exponent2, int16_t scale10, uint32_t* out) {
285
+ // Round mantissa * 2^exponent2 * 10^scale10 to the nearest integer, ties-to-even.
286
+ if (scale10 >= 0) {
287
+ _bj_big192 a; _bj_big_from_u32(&a, mantissa); _bj_big_mul5(&a, (uint8_t)scale10);
288
+ int16_t q = (int16_t)(exponent2 + scale10);
289
+ if (q >= 0) { _bj_big_shl(&a, (uint16_t)q); return _bj_big_to_u32(&a, out); }
290
+ return _bj_big_round_pow2_u32(&a, (uint16_t)(-q), out);
291
+ }
292
+ uint8_t t = (uint8_t)(-scale10);
293
+ _bj_big192 num, den;
294
+ _bj_big_from_u32(&num, mantissa);
295
+ _bj_big_from_u32(&den, 1UL); _bj_big_mul5(&den, t);
296
+ int16_t q = (int16_t)(exponent2 - t);
297
+ if (q >= 0) _bj_big_shl(&num, (uint16_t)q);
298
+ else _bj_big_shl(&den, (uint16_t)(-q));
299
+ return _bj_big_div_round_u32(&num, &den, out);
300
+ }
301
+ static void __attribute__((noinline)) _bj_f32_shortest_extreme(float value, char* digits, uint8_t* count, int16_t* exp10) {
302
+ union { float f; uint32_t u; } bits; bits.f = value;
303
+ uint32_t raw = bits.u & 0x7FFFFFFFUL;
304
+ uint32_t exp_bits = (raw >> 23) & 0xFFUL;
305
+ uint32_t fraction = raw & 0x7FFFFFUL;
306
+ uint32_t mantissa;
307
+ int16_t exponent2;
308
+ if (exp_bits == 0) { mantissa = fraction; exponent2 = -149; }
309
+ else { mantissa = (1UL << 23) | fraction; exponent2 = (int16_t)exp_bits - 150; }
310
+
311
+ // Exact lower/upper rounding boundaries, expressed as boundaryMantissa * 2^(exponent2-2).
312
+ // Powers of two have the closer lower neighbour; all other values use symmetric spacing.
313
+ uint32_t mm_shift = (fraction != 0 || exp_bits <= 1) ? 1UL : 0UL;
314
+ uint32_t lower_m = 4UL * mantissa - 1UL - mm_shift;
315
+ uint32_t upper_m = 4UL * mantissa + 2UL;
316
+ int16_t boundary_e2 = (int16_t)(exponent2 - 2);
317
+ bool accept_boundary = (mantissa & 1UL) == 0; // IEEE ties-to-even.
318
+
319
+ uint8_t mbits = 0; for (uint32_t t = mantissa; t; t >>= 1) ++mbits;
320
+ int16_t floor_log2 = (int16_t)(exponent2 + (int16_t)mbits - 1);
321
+ int32_t prod = (int32_t)floor_log2 * 1233L; // log10(2) approximation.
322
+ int16_t k = prod >= 0 ? (int16_t)(prod / 4096L)
323
+ : (int16_t)(-(((-prod) + 4095L) / 4096L));
324
+ for (uint8_t fix = 0; fix < 6; ++fix) {
325
+ // Compare 10^k to the exact binary32 value.
326
+ if (_bj_dec_vs_binary(1UL, (int16_t)(-k), mantissa, exponent2) > 0) { --k; continue; }
327
+ if (_bj_dec_vs_binary(1UL, (int16_t)(-(k + 1)), mantissa, exponent2) <= 0) { ++k; continue; }
328
+ break;
329
+ }
330
+
331
+ uint32_t low = 1UL;
332
+ for (uint8_t precision = 1; precision <= 9; ++precision, low *= 10UL) {
333
+ uint32_t high = low * 10UL;
334
+ int16_t trial_k = k;
335
+ for (uint8_t adjust = 0; adjust < 4; ++adjust) {
336
+ int16_t scale10 = (int16_t)precision - 1 - trial_k;
337
+ uint32_t n = 0;
338
+ if (!_bj_scaled_decimal_round(mantissa, exponent2, scale10, &n)) break;
339
+ if (n >= high) { ++trial_k; continue; }
340
+ if (n < low) { --trial_k; continue; }
341
+ int8_t lo_cmp = _bj_dec_vs_binary(n, scale10, lower_m, boundary_e2);
342
+ int8_t hi_cmp = _bj_dec_vs_binary(n, scale10, upper_m, boundary_e2);
343
+ bool inside = accept_boundary ? (lo_cmp >= 0 && hi_cmp <= 0)
344
+ : (lo_cmp > 0 && hi_cmp < 0);
345
+ if (!inside) break;
346
+ uint32_t x = n;
347
+ for (int8_t i = (int8_t)precision - 1; i >= 0; --i) {
348
+ digits[i] = (char)('0' + (x % 10UL)); x /= 10UL;
349
+ }
350
+ *count = precision;
351
+ *exp10 = trial_k;
352
+ return;
353
+ }
354
+ }
355
+
356
+ // IEEE-754 binary32 always has a 9-significant-digit round-trip decimal.
357
+ // This defensive path remains non-halting even if a future compiler change
358
+ // violates an assumption in the shortest search.
359
+ int16_t scale10 = (int16_t)(8 - k);
360
+ uint32_t n = 0;
361
+ if (!_bj_scaled_decimal_round(mantissa, exponent2, scale10, &n)) n = mantissa;
362
+ if (n >= 1000000000UL) { n /= 10UL; ++k; }
363
+ while (n < 100000000UL) { n *= 10UL; --k; }
364
+ for (int8_t i = 8; i >= 0; --i) { digits[i] = (char)('0' + (n % 10UL)); n /= 10UL; }
365
+ *count = 9;
366
+ *exp10 = k;
367
+ }
368
+ static void __attribute__((noinline)) _bj_print_py_float(Print& out, float value) {
369
+ if (isnan(value)) { out.print(F("nan")); return; }
370
+ if (isinf(value)) { out.print(value < 0.0f ? F("-inf") : F("inf")); return; }
371
+ if (value == 0.0f) {
372
+ union { float f; uint32_t u; } bits; bits.f = value;
373
+ out.print((bits.u & 0x80000000UL) ? F("-0.0") : F("0.0"));
374
+ return;
375
+ }
376
+ char result[32]; char* w = result;
377
+ char digits[10]; uint8_t count = 0;
378
+ int16_t exponent = 0;
379
+ if (value < 0.0f) *w++ = '-';
380
+ if (!_bj_f32_shortest(value, digits, &count, &exponent)) {
381
+ _bj_f32_shortest_extreme(value, digits, &count, &exponent);
382
+ }
383
+ while (count > 1 && digits[count - 1] == '0') --count;
384
+ if (exponent >= -4 && exponent < 16) {
385
+ int16_t decimal_pos = exponent + 1;
386
+ if (decimal_pos <= 0) {
387
+ *w++ = '0'; *w++ = '.';
388
+ for (int16_t i = 0; i < -decimal_pos; ++i) *w++ = '0';
389
+ for (uint8_t i = 0; i < count; ++i) *w++ = digits[i];
390
+ } else {
391
+ for (int16_t i = 0; i < decimal_pos; ++i) *w++ = (i < count) ? digits[i] : '0';
392
+ *w++ = '.';
393
+ if (decimal_pos < count) {
394
+ for (uint8_t i = (uint8_t)decimal_pos; i < count; ++i) *w++ = digits[i];
395
+ } else {
396
+ *w++ = '0';
397
+ }
398
+ }
399
+ } else {
400
+ *w++ = digits[0];
401
+ if (count > 1) { *w++ = '.'; for (uint8_t i = 1; i < count; ++i) *w++ = digits[i]; }
402
+ *w++ = 'e';
403
+ int16_t e = exponent;
404
+ if (e < 0) { *w++ = '-'; e = -e; } else { *w++ = '+'; }
405
+ char expbuf[6]; uint8_t n = 0;
406
+ do { expbuf[n++] = (char)('0' + (e % 10)); e /= 10; } while (e && n < sizeof(expbuf));
407
+ if (n < 2) *w++ = '0'; // Python prints at least two exponent digits: 1e-05
408
+ while (n) *w++ = expbuf[--n];
409
+ }
410
+ *w = '\0';
411
+ out.print(result);
412
+ }
413
+ '''
414
+
415
+ # Exact Python format(value, '.Nf') for float32 (N = 0..6). Arduino's
416
+ # Print::print(float, N) rounds half-up in float arithmetic and prints 'ovf'
417
+ # above 4294967040, which is not Python f-string behaviour.
418
+ _FIXED_HELPER_BLOB = r'''static void __attribute__((noinline)) _bj_print_py_fixed(Print& out, float value, uint8_t digits) {
419
+ // Python format(value, '.Nf') for the exact float32 value: round-half-even of value*10^N.
420
+ if (isnan(value)) { out.print(F("nan")); return; }
421
+ if (isinf(value)) { out.print(value < 0.0f ? F("-inf") : F("inf")); return; }
422
+ union { float f; uint32_t u; } bits; bits.f = value;
423
+ if (bits.u & 0x80000000UL) out.print('-');
424
+ uint32_t exp_bits = (bits.u >> 23) & 0xFFUL;
425
+ uint32_t mant = bits.u & 0x7FFFFFUL;
426
+ int16_t e2 = -149;
427
+ if (exp_bits != 0) { mant |= 0x800000UL; e2 = (int16_t)exp_bits - 150; }
428
+ uint16_t limb[10]; // little-endian base-2^16; holds up to 2^160 > 2^128 * 10^6
429
+ for (uint8_t i = 0; i < 10; ++i) limb[i] = 0;
430
+ limb[0] = (uint16_t)(mant & 0xFFFFUL);
431
+ limb[1] = (uint16_t)(mant >> 16);
432
+ for (uint8_t d = 0; d < digits; ++d) {
433
+ uint32_t carry = 0;
434
+ for (uint8_t i = 0; i < 10; ++i) {
435
+ uint32_t t = (uint32_t)limb[i] * 10UL + carry;
436
+ limb[i] = (uint16_t)(t & 0xFFFFUL);
437
+ carry = t >> 16;
438
+ }
439
+ }
440
+ if (e2 > 0) {
441
+ uint8_t q = (uint8_t)(e2 >> 4), r = (uint8_t)(e2 & 15);
442
+ for (int8_t i = 9; i >= 0; --i) {
443
+ uint32_t hi = (i - (int8_t)q >= 0) ? (uint32_t)limb[i - q] : 0UL;
444
+ uint32_t lo = (i - (int8_t)q - 1 >= 0) ? (uint32_t)limb[i - q - 1] : 0UL;
445
+ limb[i] = (uint16_t)((((hi << 16) | lo) << r) >> 16);
446
+ }
447
+ } else if (e2 < 0) {
448
+ uint8_t k = (uint8_t)(-e2);
449
+ bool half = false, sticky = false;
450
+ if (k <= 160) {
451
+ uint8_t hb = (uint8_t)(k - 1);
452
+ half = ((uint32_t)limb[hb >> 4] >> (hb & 15)) & 1UL;
453
+ for (uint8_t b = 0; b < hb && !sticky; ++b) {
454
+ if (((uint32_t)limb[b >> 4] >> (b & 15)) & 1UL) sticky = true;
455
+ }
456
+ }
457
+ uint8_t q = (uint8_t)(k >> 4), r = (uint8_t)(k & 15);
458
+ for (uint8_t i = 0; i < 10; ++i) {
459
+ uint32_t lo = (i + q < 10) ? (uint32_t)limb[i + q] : 0UL;
460
+ uint32_t hi = (i + q + 1 < 10) ? (uint32_t)limb[i + q + 1] : 0UL;
461
+ limb[i] = (uint16_t)((((hi << 16) | lo) >> r) & 0xFFFFUL);
462
+ }
463
+ if (half && (sticky || (limb[0] & 1U))) {
464
+ for (uint8_t i = 0; i < 10; ++i) { if (++limb[i] != 0) break; }
465
+ }
466
+ }
467
+ char buf[50];
468
+ uint8_t n = 0;
469
+ for (;;) {
470
+ uint32_t rem = 0;
471
+ bool more = false;
472
+ for (int8_t i = 9; i >= 0; --i) {
473
+ uint32_t cur = (rem << 16) | (uint32_t)limb[i];
474
+ limb[i] = (uint16_t)(cur / 10UL);
475
+ rem = cur % 10UL;
476
+ if (limb[i]) more = true;
477
+ }
478
+ buf[n++] = (char)('0' + (char)rem);
479
+ if (!more) break;
480
+ }
481
+ while (n <= digits) buf[n++] = '0';
482
+ for (uint8_t i = n; i-- > digits;) out.print(buf[i]);
483
+ if (digits) {
484
+ out.print('.');
485
+ for (uint8_t i = digits; i-- > 0;) out.print(buf[i]);
486
+ }
487
+ }
488
+ '''
489
+
490
+ _HELPER_DEPS: dict[str, tuple[str, ...]] = {
491
+ "_bj_list_index": ("_bj_numeric_fail",),
492
+ "_bj_i32_add": ("_bj_numeric_fail",),
493
+ "_bj_i32_sub": ("_bj_numeric_fail",),
494
+ "_bj_i32_mul": ("_bj_numeric_fail",),
495
+ "_bj_i32_neg": ("_bj_numeric_fail",),
496
+ "_bj_true_div_f32": ("_bj_numeric_fail",),
497
+ "_bj_mod_i32": ("_bj_numeric_fail",),
498
+ "_bj_mod_f32": ("_bj_numeric_fail",),
499
+ "_bj_pwm_value": ("_bj_numeric_fail",),
500
+ "_bj_byte_value": ("_bj_numeric_fail",),
501
+ "_bj_digital_level": ("_bj_numeric_fail",),
502
+ "_bj_pin_mode": ("_bj_numeric_fail",),
503
+ "_bj_i2c_address": ("_bj_numeric_fail",),
504
+ "_bj_arduino_format": ("_bj_numeric_fail",),
505
+ "_bj_wire_count": ("_bj_numeric_fail",),
506
+ "_bj_nonnegative_int": ("_bj_numeric_fail",),
507
+ "_bj_lcd_dimension": ("_bj_numeric_fail",),
508
+ "_bj_lcd_charsize": ("_bj_numeric_fail",),
509
+ "_bj_lcd_cursor_column": ("_bj_numeric_fail",),
510
+ "_bj_lcd_cursor_row": ("_bj_numeric_fail",),
511
+ "_bj_positive_int": ("_bj_numeric_fail",),
512
+ "_bj_servo_angle": ("_bj_numeric_fail",),
513
+ "_bj_servo_pulse": ("_bj_numeric_fail",),
514
+ "_bj_servo_attach_bounds": ("_bj_numeric_fail",),
515
+ "_bj_digital_pin": ("_bj_numeric_fail",),
516
+ "_bj_pwm_pin": ("_bj_numeric_fail",),
517
+ "_bj_analog_pin": ("_bj_numeric_fail",),
518
+ "_bj_time_threshold": ("_bj_numeric_fail",),
519
+ "_bj_float_to_i32": ("_bj_numeric_fail",),
520
+ "_bj_round_i32": ("_bj_round_even_f32", "_bj_numeric_fail"),
521
+ "_bj_round_f32_digits": (
522
+ "_bj_round_ratio_even_u64", "_bj_round_pow2_even_u64", "_bj_ratio_to_f32", "_bj_numeric_fail",
523
+ ),
524
+ "_bj_round_i32_digits": ("_bj_numeric_fail",),
525
+ "_bj_i32_abs": ("_bj_numeric_fail",),
526
+ }
527
+
528
+
529
+ def _package_version() -> str:
530
+ from .. import __version__
531
+ return __version__
532
+
533
+
534
+ def _helper_name(line: str) -> str:
535
+ """Name of a `static ... _bj_name(` helper definition line (attributes allowed)."""
536
+ match = re.search(r"\b(_bj_\w+)\s*\(", line)
537
+ if match is None:
538
+ raise InternalCompilerError(f"Malformed backend helper definition: {line!r}")
539
+ return match.group(1)
540
+
541
+
542
+ def _split_helper_blob(blob: str) -> dict[str, str]:
543
+ lines = blob.splitlines()
544
+ result: dict[str, str] = {}
545
+ i = 0
546
+ while i < len(lines):
547
+ line = lines[i]
548
+ if not line.startswith("static "):
549
+ i += 1
550
+ continue
551
+ name = _helper_name(line)
552
+ depth = 0
553
+ block: list[str] = []
554
+ while i < len(lines):
555
+ current = lines[i]
556
+ block.append(current)
557
+ depth += current.count("{") - current.count("}")
558
+ i += 1
559
+ if depth == 0:
560
+ break
561
+ result[name] = "\n".join(block)
562
+ return result
563
+
564
+
565
+ # Helpers for the upload-only Arduino core additions in V1.1 Stage 1.
566
+ _NUMERIC_HELPER_BLOB += r'''
567
+ static uint16_t _bj_tone_frequency(int32_t hz) {
568
+ if (hz < 31 || hz > 65535) _bj_numeric_fail();
569
+ return (uint16_t)hz;
570
+ }
571
+ static uint16_t _bj_delay_us(int32_t microseconds) {
572
+ if (microseconds < 0 || microseconds > 16383) _bj_numeric_fail();
573
+ return (uint16_t)microseconds;
574
+ }
575
+ static int32_t _bj_constrain_int(int32_t value, int32_t low, int32_t high) {
576
+ if (low > high) _bj_numeric_fail();
577
+ return value < low ? low : (value > high ? high : value);
578
+ }
579
+ static int32_t _bj_arduino_map(int32_t value, int32_t in_min, int32_t in_max, int32_t out_min, int32_t out_max) {
580
+ int64_t den = (int64_t)in_max - (int64_t)in_min;
581
+ if (!den) _bj_numeric_fail();
582
+ int64_t left = (int64_t)value - (int64_t)in_min;
583
+ int64_t right = (int64_t)out_max - (int64_t)out_min;
584
+ // Arduino map() truncates division toward zero. Using 64-bit intermediates
585
+ // avoids the usual 32-bit overflow in classroom joystick mappings.
586
+ // Reject extreme inputs that cannot be represented safely as signed 64-bit.
587
+ uint64_t a = left < 0 ? (uint64_t)(-left) : (uint64_t)left;
588
+ uint64_t b = right < 0 ? (uint64_t)(-right) : (uint64_t)right;
589
+ if (b && a > 9223372036854775807ULL / b) _bj_numeric_fail();
590
+ int64_t num = left * right;
591
+ int64_t result = num / den + (int64_t)out_min;
592
+ if (result < -2147483648LL || result > 2147483647LL) _bj_numeric_fail();
593
+ return (int32_t)result;
594
+ }
595
+ '''
596
+ _HELPER_DEPS.update({
597
+ "_bj_tone_frequency": ("_bj_numeric_fail",),
598
+ "_bj_delay_us": ("_bj_numeric_fail",),
599
+ "_bj_constrain_int": ("_bj_numeric_fail",),
600
+ "_bj_arduino_map": ("_bj_numeric_fail",),
601
+ })
602
+ _HELPER_BLOCKS = _split_helper_blob(_NUMERIC_HELPER_BLOB)
603
+
604
+
605
+ _PIN_GUARDS = {
606
+ "pinMode": "_bj_digital_pin", "digitalWrite": "_bj_digital_pin", "digitalRead": "_bj_digital_pin",
607
+ "analogWrite": "_bj_pwm_pin", "analogRead": "_bj_analog_pin", "tone": "_bj_digital_pin", "noTone": "_bj_digital_pin",
608
+ }
609
+ _PIN_SETS = {
610
+ "_bj_digital_pin": frozenset(range(20)),
611
+ "_bj_pwm_pin": frozenset({3, 5, 6, 9, 10, 11}),
612
+ "_bj_analog_pin": frozenset(set(range(6)) | set(range(14, 20))),
613
+ }
614
+
615
+
616
+ def _helper_closure(names: set[str]) -> set[str]:
617
+ closure = set(names); changed = True
618
+ while changed:
619
+ changed = False
620
+ for name in tuple(closure):
621
+ for dep in _HELPER_DEPS.get(name, ()):
622
+ if dep not in closure: closure.add(dep); changed = True
623
+ return closure
624
+
625
+
626
+ def _walk(value: Any):
627
+ yield value
628
+ if is_dataclass(value):
629
+ for f in fields(value):
630
+ yield from _walk(getattr(value, f.name))
631
+ elif isinstance(value, (tuple, list, frozenset)):
632
+ for item in value:
633
+ yield from _walk(item)
634
+ elif isinstance(value, dict):
635
+ for key, item in value.items():
636
+ yield from _walk(key)
637
+ yield from _walk(item)
638
+
639
+
640
+ class UnoCppBackend:
641
+ """Phase-H backend. Its only semantic inputs are typed IR + TargetPlan."""
642
+
643
+ def __init__(self, ir_module: IRModule, target_plan: TargetPlan) -> None:
644
+ validate_ir(ir_module)
645
+ if target_plan.ir_module is not ir_module and target_plan.ir_module != ir_module:
646
+ raise InternalCompilerError("TargetPlan does not belong to the supplied typed IR module.")
647
+ self.ir = ir_module
648
+ self.target = target_plan
649
+ self._temp_counter = 0
650
+ self._temp_names: dict[object, str] = {}
651
+ self._storage = {decl.symbol.symbol_id: decl for decl in ir_module.storages}
652
+ self._objects = {obj.symbol.symbol_id: obj for obj in ir_module.objects}
653
+ self._hoisted_stmt_ids: set[int] = set()
654
+ self._dynamic_helpers: set[str] = set()
655
+ self._constant_symbol_values: dict[object, int] = {}
656
+ # Range facts are derived only from typed IR. They let the backend
657
+ # omit redundant runtime guards without reaching back into numeric AST analysis.
658
+ self._active_int_ranges: dict[object, tuple[int, int]] = {}
659
+ self._safe_pwm_call_ids: set[int] = self._find_range_proven_pwm_calls()
660
+
661
+ def _find_range_proven_pwm_calls(self) -> set[int]:
662
+ safe: set[int] = set()
663
+
664
+ def visit_expr(expr: IRExpr, ranges: dict[object, tuple[int, int]]) -> None:
665
+ if isinstance(expr, IRCall):
666
+ if (expr.kind == IRCallKind.API_FUNCTION and expr.name == "analogWrite"
667
+ and len(expr.args) >= 2 and self._known_pwm_safe(expr.args[1], ranges)):
668
+ safe.add(id(expr))
669
+ if is_dataclass(expr):
670
+ for f in fields(expr):
671
+ value = getattr(expr, f.name)
672
+ if isinstance(value, IRExpr):
673
+ visit_expr(value, ranges)
674
+ elif isinstance(value, (tuple, list)):
675
+ for item in value:
676
+ if isinstance(item, IRExpr):
677
+ visit_expr(item, ranges)
678
+ elif isinstance(item, IRFormattedValue):
679
+ visit_expr(item.value, ranges)
680
+
681
+ def visit_stmt(stmt: IRStmt, ranges: dict[object, tuple[int, int]]) -> None:
682
+ local = ranges
683
+ if isinstance(stmt, IRRangeLoop):
684
+ facts = self._constant_range_bounds(stmt)
685
+ body_ranges = dict(ranges)
686
+ if facts is not None and not self._body_writes(stmt.body, stmt.target.symbol_id, stmt.target.kind != SymbolKind.LOCAL):
687
+ body_ranges[stmt.target.symbol_id] = facts
688
+ visit_expr(stmt.start, ranges); visit_expr(stmt.stop, ranges); visit_expr(stmt.step, ranges)
689
+ for child in stmt.body:
690
+ visit_stmt(child, body_ranges)
691
+ return
692
+ if isinstance(stmt, IRIf):
693
+ visit_expr(stmt.condition, ranges)
694
+ for child in stmt.body: visit_stmt(child, dict(ranges))
695
+ for child in stmt.otherwise: visit_stmt(child, dict(ranges))
696
+ return
697
+ if isinstance(stmt, IRWhile):
698
+ visit_expr(stmt.condition, ranges)
699
+ for child in stmt.body: visit_stmt(child, dict(ranges))
700
+ return
701
+ if isinstance(stmt, IRFixedListLoop):
702
+ if stmt.index_start is not None: visit_expr(stmt.index_start, ranges)
703
+ for child in stmt.body: visit_stmt(child, dict(ranges))
704
+ return
705
+ if is_dataclass(stmt):
706
+ for f in fields(stmt):
707
+ value = getattr(stmt, f.name)
708
+ if isinstance(value, IRExpr): visit_expr(value, local)
709
+ elif isinstance(value, (tuple, list)):
710
+ for item in value:
711
+ if isinstance(item, IRExpr): visit_expr(item, local)
712
+
713
+ for stmt in self.ir.setup: visit_stmt(stmt, {})
714
+ for stmt in self.ir.main_loop: visit_stmt(stmt, {})
715
+ for fn in self.ir.functions:
716
+ for stmt in fn.body: visit_stmt(stmt, {})
717
+ return safe
718
+
719
+ @staticmethod
720
+ def _body_writes(body, symbol_id, global_symbol: bool = True) -> bool:
721
+ for item in _walk(body):
722
+ if isinstance(item, (IRStore, IRAugStore)) and item.target.symbol_id == symbol_id:
723
+ return True
724
+ if isinstance(item, IRRangeLoop) and item.target.symbol_id == symbol_id:
725
+ return True
726
+ if isinstance(item, IRFixedListLoop) and (item.value_target.symbol_id == symbol_id or (item.index_target is not None and item.index_target.symbol_id == symbol_id)):
727
+ return True
728
+ if global_symbol and isinstance(item, IRCall) and item.kind == IRCallKind.USER_FUNCTION:
729
+ return True # a called function may write a module-level loop variable
730
+ return False
731
+
732
+ def _constant_range_bounds(self, stmt: IRRangeLoop) -> tuple[int, int] | None:
733
+ start, stop, step = self._const_int(stmt.start), self._const_int(stmt.stop), self._const_int(stmt.step)
734
+ if start is None or stop is None or step in (None, 0):
735
+ return None
736
+ if step > 0:
737
+ if start >= stop: return None
738
+ last = start + ((stop - 1 - start) // step) * step
739
+ return (start, last)
740
+ if start <= stop: return None
741
+ last = start + ((start - (stop + 1)) // (-step)) * step
742
+ return (last, start)
743
+
744
+ def generate(self) -> GeneratedSketch:
745
+ helper_names = self._required_numeric_helpers()
746
+ if self._needs_python_float_formatter():
747
+ # The exact float32 shortest-repr formatter reuses the exact rational helpers.
748
+ helper_names |= {"_bj_round_ratio_even_u64", "_bj_round_pow2_even_u64", "_bj_ratio_to_f32", "_bj_numeric_fail"}
749
+ parts: list[tuple[str, SourceSpan | None]] = []
750
+ add = parts.append
751
+
752
+ add((f"// Generated by Barmaja PyArduino {_package_version()}", None))
753
+ add(("// Edit the Python source, not this generated file.", None))
754
+ for header in self.target.includes:
755
+ add((f"#include <{header}>", None))
756
+ add(("", None))
757
+
758
+ # Arduino's .ino preprocessor synthesizes prototypes before the first function
759
+ # definition. Helper-owned types therefore MUST appear before every helper
760
+ # function; otherwise ctags can place prototypes above their typedefs (F7).
761
+ if self._needs_python_float_formatter():
762
+ add((_OUTPUT_HELPER_PREAMBLE, None))
763
+ add(("", None))
764
+
765
+ # Helpers are inserted here after the body is generated, so runtime guards
766
+ # chosen during generation (delay/servo/timeout) are emitted exactly when used.
767
+ helper_insert_at = len(parts)
768
+
769
+ hoisted = self._find_hoistable_setup_stores()
770
+ # Module constants initialized statically have exactly one known value.
771
+ for sid, store in hoisted.items():
772
+ decl = self._storage.get(sid)
773
+ value = self._const_int(store.value)
774
+ if decl is not None and decl.storage == StorageClass.MODULE_CONSTANT and value is not None:
775
+ self._constant_symbol_values[sid] = value
776
+ emitted_globals = False
777
+ for decl in self.ir.storages:
778
+ if decl.storage not in {StorageClass.MODULE_CONSTANT, StorageClass.PERSISTENT_MODULE}:
779
+ continue
780
+ emitted_globals = True
781
+ init_stmt = hoisted.get(decl.symbol.symbol_id)
782
+ if init_stmt is not None:
783
+ self._hoisted_stmt_ids.add(id(init_stmt))
784
+ value = init_stmt.value
785
+ if decl.symbol.type.kind == TypeKind.FIXED_LIST:
786
+ if not isinstance(value, IRListLiteral):
787
+ raise InternalCompilerError("Fixed-list static initializer is not an IR list literal.")
788
+ elem = decl.symbol.type.element_type
789
+ if elem is None or decl.symbol.type.length is None:
790
+ raise InternalCompilerError("Incomplete fixed-list type reached Uno backend.")
791
+ items = ", ".join(self._expr(x) for x in value.elements)
792
+ if decl.storage == StorageClass.MODULE_CONSTANT:
793
+ # `const char*` elements: make the pointers const (`const char* const`),
794
+ # never the ill-formed `const const char*`.
795
+ elem_decl = f"{elem.cpp_type} const" if elem.cpp_type.startswith("const ") else f"const {elem.cpp_type}"
796
+ else:
797
+ elem_decl = elem.cpp_type
798
+ add((f"{elem_decl} {decl.symbol.backend_name}[{decl.symbol.type.length}] = {{{items}}};", decl.span))
799
+ else:
800
+ typ = decl.symbol.type.cpp_type
801
+ dtype = ("const char* const" if typ == "const char*" else f"const {typ}") if decl.storage == StorageClass.MODULE_CONSTANT else typ
802
+ add((f"{dtype} {decl.symbol.backend_name} = {self._expr(value)};", decl.span))
803
+ else:
804
+ add((self._decl_line(decl.symbol), decl.span))
805
+ if emitted_globals:
806
+ add(("", None))
807
+
808
+ ctor_values = dict(self.target.object_arg_values)
809
+ for obj in self.ir.objects:
810
+ args = ", ".join(self._ctor_arg(a, v) for a, v in zip(obj.args, ctor_values.get(obj.symbol.symbol_id, (None,) * len(obj.args))))
811
+ # Arduino Ethernet.h declares EthernetW5500 as a global enum value.
812
+ # Keep the public Python hardware-class name, but emit our uniquely
813
+ # named C++ adapter type to avoid collision with the Arduino library.
814
+ cpp_class_name = "BarmajaEthernetW5500" if obj.class_name == "EthernetW5500" else obj.class_name
815
+ text = f"{cpp_class_name} {obj.symbol.backend_name}({args});" if args else f"{cpp_class_name} {obj.symbol.backend_name};"
816
+ add((text, obj.span))
817
+ if self.ir.objects:
818
+ add(("", None))
819
+
820
+ for fn in self.ir.functions:
821
+ params = ", ".join(f"{p.type.cpp_type} {p.backend_name}" for p in fn.parameters)
822
+ add((f"{fn.return_type.cpp_type} {fn.symbol.backend_name}({params});", fn.span))
823
+ if self.ir.functions:
824
+ add(("", None))
825
+
826
+ for fn in self.ir.functions:
827
+ parts.extend(self._function(fn))
828
+ add(("", None))
829
+
830
+ add(("void setup() {", None))
831
+ for decl in self.ir.storages:
832
+ if decl.storage == StorageClass.SETUP_LOCAL:
833
+ add((" " + self._decl_line(decl.symbol, local=True), decl.span))
834
+ for hook in self.target.setup_hooks:
835
+ add((" " + hook, None))
836
+ for stmt in self.ir.setup:
837
+ if id(stmt) in self._hoisted_stmt_ids:
838
+ continue
839
+ parts.extend(self._stmt(stmt, 1))
840
+ add(("}", None))
841
+ add(("", None))
842
+
843
+ add(("void loop() {", None))
844
+ for decl in self.ir.storages:
845
+ if decl.storage == StorageClass.LOOP_TEMPORARY:
846
+ add((" " + self._decl_line(decl.symbol, local=True), decl.span))
847
+ for stmt in self.ir.main_loop:
848
+ parts.extend(self._stmt(stmt, 1))
849
+ add(("}", None))
850
+ add(("", None))
851
+
852
+ helper_names = _helper_closure(helper_names | self._dynamic_helpers)
853
+ helper_lines: list[tuple[str, SourceSpan | None]] = []
854
+ if helper_names:
855
+ for name in self._helper_order(helper_names):
856
+ block = _HELPER_BLOCKS.get(name)
857
+ if block is None:
858
+ raise InternalCompilerError(f"Backend helper '{name}' is not defined.")
859
+ helper_lines.extend((line, None) for line in block.splitlines())
860
+ helper_lines.append(("", None))
861
+ if self._needs_python_float_formatter():
862
+ helper_lines.extend((line, None) for line in _OUTPUT_HELPER_BLOB.splitlines())
863
+ helper_lines.append(("", None))
864
+ if self._needs_fixed_float_formatter():
865
+ helper_lines.extend((line, None) for line in _FIXED_HELPER_BLOB.splitlines())
866
+ helper_lines.append(("", None))
867
+ parts[helper_insert_at:helper_insert_at] = helper_lines
868
+
869
+ cpp_lines = [text for text, _ in parts]
870
+ source_map = tuple(
871
+ SourceMapEntry(index + 1, self.ir.source_name, span)
872
+ for index, (_, span) in enumerate(parts) if span is not None
873
+ )
874
+ return GeneratedSketch(
875
+ cpp="\n".join(cpp_lines),
876
+ source_map=source_map,
877
+ dependency_ids=self.target.dependency_ids,
878
+ resolved_serial_baud=self.target.resolved_serial_baud,
879
+ diagnostics=self.target.diagnostics,
880
+ )
881
+
882
+ def _ctor_arg(self, arg: IRExpr, value: int | None) -> str:
883
+ """Hardware objects are constructed before setup(); never reference setup-time storage."""
884
+ if isinstance(arg, IRConstant) or (isinstance(arg, IRLoad) and arg.symbol.kind in {SymbolKind.API, SymbolKind.BUILTIN}):
885
+ return self._expr(arg)
886
+ if value is None:
887
+ raise InternalCompilerError("Hardware constructor argument reached the backend without a proven compile-time value.")
888
+ return str(value)
889
+
890
+ def _find_hoistable_setup_stores(self) -> dict[object, IRStore]:
891
+ """Find setup stores that may become file-scope static initializers.
892
+
893
+ A store is only hoisted when it is the first write of that symbol in
894
+ execution order. Any earlier setup statement that may write the symbol
895
+ (nested stores, loop targets, augmented/index stores, or a user-function
896
+ call that could write globals) blocks hoisting, because moving the store
897
+ before those writes would change the program's result.
898
+ """
899
+ result: dict[object, IRStore] = {}
900
+ written: set[object] = set()
901
+ opaque_call_seen = False
902
+ for stmt in self.ir.setup:
903
+ if isinstance(stmt, IRStore):
904
+ sid = stmt.target.symbol_id
905
+ decl = self._storage.get(sid)
906
+ if (
907
+ decl is not None
908
+ and decl.storage in {StorageClass.MODULE_CONSTANT, StorageClass.PERSISTENT_MODULE}
909
+ and sid not in written
910
+ and sid not in result
911
+ and (decl.storage == StorageClass.MODULE_CONSTANT or not opaque_call_seen)
912
+ and self._is_static_initializer(stmt.value)
913
+ and not (stmt.target.type.kind == TypeKind.TIME_U32 and not self._known_nonnegative_int(stmt.value))
914
+ ):
915
+ result[sid] = stmt
916
+ for item in _walk(stmt):
917
+ if isinstance(item, (IRStore, IRAugStore)):
918
+ written.add(item.target.symbol_id)
919
+ elif isinstance(item, IRIndexStore):
920
+ written.add(item.collection.symbol_id)
921
+ elif isinstance(item, IRRangeLoop):
922
+ written.add(item.target.symbol_id)
923
+ elif isinstance(item, IRFixedListLoop):
924
+ written.add(item.value_target.symbol_id)
925
+ if item.index_target is not None:
926
+ written.add(item.index_target.symbol_id)
927
+ elif isinstance(item, IRCall) and item.kind == IRCallKind.USER_FUNCTION:
928
+ opaque_call_seen = True
929
+ return result
930
+
931
+ def _is_static_initializer(self, expr: IRExpr) -> bool:
932
+ if isinstance(expr, IRConstant):
933
+ return True
934
+ if isinstance(expr, IRUnary) and expr.op == IRUnaryOp.NEGATE and isinstance(expr.operand, IRConstant):
935
+ return True
936
+ if isinstance(expr, IRListLiteral):
937
+ return all(self._is_static_initializer(x) for x in expr.elements)
938
+ return False
939
+
940
+ @staticmethod
941
+ def _decl_line(symbol: IRSymbolRef, *, local: bool = False) -> str:
942
+ typ = symbol.type
943
+ if typ.kind == TypeKind.FIXED_LIST:
944
+ if typ.element_type is None or typ.length is None:
945
+ raise InternalCompilerError("Incomplete fixed-list type reached declaration generation.")
946
+ # A fixed list inside setup()/loop()/a function would otherwise live on the
947
+ # 2 KB AVR stack, invisible to both the static SRAM estimate and the
948
+ # linker's "Global variables" figure. Static storage is safe here: V1
949
+ # forbids recursion and every local list is definitely assigned before use.
950
+ storage = "static " if local else ""
951
+ return f"{storage}{typ.element_type.cpp_type} {symbol.backend_name}[{typ.length}];"
952
+ return f"{typ.cpp_type} {symbol.backend_name};"
953
+
954
+ def _function(self, fn: IRFunction) -> list[tuple[str, SourceSpan | None]]:
955
+ params = ", ".join(f"{p.type.cpp_type} {p.backend_name}" for p in fn.parameters)
956
+ out: list[tuple[str, SourceSpan | None]] = [(f"{fn.return_type.cpp_type} {fn.symbol.backend_name}({params}) {{", fn.span)]
957
+ for symbol in self._function_locals(fn):
958
+ out.append((" " + self._decl_line(symbol, local=True), fn.span))
959
+ for stmt in fn.body:
960
+ out.extend(self._stmt(stmt, 1))
961
+ out.append(("}", fn.span))
962
+ return out
963
+
964
+ def _function_locals(self, fn: IRFunction) -> list[IRSymbolRef]:
965
+ params = {p.symbol_id for p in fn.parameters}
966
+ module_ids = set(self._storage) | set(self._objects)
967
+ found: dict[object, IRSymbolRef] = {}
968
+ for item in _walk(fn.body):
969
+ refs: list[IRSymbolRef] = []
970
+ if isinstance(item, (IRStore, IRAugStore)):
971
+ refs.append(item.target)
972
+ elif isinstance(item, IRFixedListLoop):
973
+ refs.append(item.value_target)
974
+ if item.index_target is not None:
975
+ refs.append(item.index_target)
976
+ elif isinstance(item, IRRangeLoop):
977
+ refs.append(item.target)
978
+ for ref in refs:
979
+ if ref.symbol_id not in params and ref.symbol_id not in module_ids and ref.kind == SymbolKind.LOCAL:
980
+ found.setdefault(ref.symbol_id, ref)
981
+ return sorted(found.values(), key=lambda s: s.symbol_id.value)
982
+
983
+ def _aug_store_lines(self, stmt: IRAugStore, pad: str, span, current: str, rhs: str) -> list[tuple[str, SourceSpan | None]]:
984
+ name = stmt.target.backend_name
985
+ kind = stmt.target.type.kind
986
+ if stmt.op == IRBinaryOp.TRUE_DIVIDE:
987
+ return [(f"{pad}{name} = _bj_true_div_f32((float)({current}), (float)({rhs}));", span)]
988
+ if stmt.op == IRBinaryOp.MODULO:
989
+ if kind == TypeKind.FLOAT:
990
+ return [(f"{pad}{name} = _bj_mod_f32((float)({current}), (float)({rhs}));", span)]
991
+ return [(f"{pad}{name} = _bj_mod_i32({current}, {rhs});", span)]
992
+ if kind == TypeKind.INT and stmt.op in {IRBinaryOp.ADD, IRBinaryOp.SUBTRACT, IRBinaryOp.MULTIPLY}:
993
+ helper = {IRBinaryOp.ADD:"_bj_i32_add", IRBinaryOp.SUBTRACT:"_bj_i32_sub", IRBinaryOp.MULTIPLY:"_bj_i32_mul"}[stmt.op]
994
+ return [(f"{pad}{name} = {helper}({current}, {rhs});", span)]
995
+ op = {IRBinaryOp.ADD:"+", IRBinaryOp.SUBTRACT:"-", IRBinaryOp.MULTIPLY:"*"}.get(stmt.op)
996
+ if op is None:
997
+ raise InternalCompilerError("Unsupported augmented IR operator reached Uno backend.")
998
+ return [(f"{pad}{name} = ({current} {op} {rhs});", span)]
999
+
1000
+ def _stmt(self, stmt: IRStmt, indent: int) -> list[tuple[str, SourceSpan | None]]:
1001
+ pad = " " * indent
1002
+ span = stmt.span
1003
+ if isinstance(stmt, IRStore):
1004
+ if stmt.target.type.kind == TypeKind.FIXED_LIST:
1005
+ if not isinstance(stmt.value, IRListLiteral):
1006
+ raise InternalCompilerError("Whole fixed-list store reached backend without IRListLiteral.")
1007
+ elements = stmt.value.elements
1008
+ if all(isinstance(v, IRConstant) for v in elements):
1009
+ return [(f"{pad}{stmt.target.backend_name}[{i}] = {self._expr(v)};", span) for i, v in enumerate(elements)]
1010
+ # Python evaluates the whole list literal (left to right) before
1011
+ # rebinding: `L = [x, L[0], L[1]]` must read the OLD elements.
1012
+ # Materialize every element first, then store.
1013
+ elem_type = stmt.target.type.element_type
1014
+ if elem_type is None:
1015
+ raise InternalCompilerError("Incomplete fixed-list type reached Uno backend.")
1016
+ temps = [self._new_temp("list_elem") for _ in elements]
1017
+ out = [(f"{pad}{elem_type.cpp_type} {t} = {self._expr(v)};", span) for t, v in zip(temps, elements)]
1018
+ out.extend((f"{pad}{stmt.target.backend_name}[{i}] = {t};", span) for i, t in enumerate(temps))
1019
+ return out
1020
+ value = self._expr(stmt.value)
1021
+ if stmt.target.type.kind == TypeKind.TIME_U32 and stmt.value.type.kind == TypeKind.INT and not self._known_nonnegative_int(stmt.value):
1022
+ value = f"_bj_time_threshold({value})"; self._dynamic_helpers.add("_bj_time_threshold")
1023
+ return [(f"{pad}{stmt.target.backend_name} = {value};", span)]
1024
+ if isinstance(stmt, IRIndexStore):
1025
+ if self._expr_has_effects(stmt.value):
1026
+ # Python evaluates the right-hand side before the subscript target.
1027
+ value_tmp = self._new_temp("store_value")
1028
+ value_line = f"{pad}{stmt.value.type.cpp_type} {value_tmp} = {self._expr(stmt.value)};"
1029
+ index = self._list_index_expr(stmt.index, stmt.collection.type)
1030
+ return [(value_line, span), (f"{pad}{stmt.collection.backend_name}[{index}] = {value_tmp};", span)]
1031
+ index = self._list_index_expr(stmt.index, stmt.collection.type)
1032
+ return [(f"{pad}{stmt.collection.backend_name}[{index}] = {self._expr(stmt.value)};", span)]
1033
+ if isinstance(stmt, IRAugStore):
1034
+ name = stmt.target.backend_name
1035
+ rhs = self._expr(stmt.value)
1036
+ prefix: list[tuple[str, SourceSpan | None]] = []
1037
+ if self._expr_has_effects(stmt.value):
1038
+ # Python reads the target before evaluating the right-hand side.
1039
+ old_tmp = self._new_temp("aug_target")
1040
+ rhs_tmp = self._new_temp("aug_value")
1041
+ prefix = [
1042
+ (f"{pad}{stmt.target.type.cpp_type} {old_tmp} = {name};", span),
1043
+ (f"{pad}{stmt.value.type.cpp_type} {rhs_tmp} = {rhs};", span),
1044
+ ]
1045
+ return prefix + self._aug_store_lines(stmt, pad, span, old_tmp, rhs_tmp)
1046
+ return self._aug_store_lines(stmt, pad, span, name, rhs)
1047
+ if isinstance(stmt, IRExprStmt):
1048
+ special = self._explicit_output_stmt(stmt.expression, indent)
1049
+ if special is not None:
1050
+ return [(line, span) for line in special]
1051
+ return [(f"{pad}{self._expr(stmt.expression)};", span)]
1052
+ if isinstance(stmt, IRPrint):
1053
+ return [(line, span) for line in self._python_print(stmt, indent)]
1054
+ if isinstance(stmt, IRReturn):
1055
+ return [(pad + ("return;" if stmt.value is None else f"return {self._expr(stmt.value)};"), span)]
1056
+ if isinstance(stmt, IRBreak):
1057
+ if stmt.target == IRLoopControlTarget.MAIN_LOOP:
1058
+ raise InternalCompilerError("A main-loop break reached the Uno backend after validation.")
1059
+ return [(pad + "break;", span)]
1060
+ if isinstance(stmt, IRContinue):
1061
+ return [(pad + ("return;" if stmt.target == IRLoopControlTarget.MAIN_LOOP else "continue;"), span)]
1062
+ if isinstance(stmt, IRIf):
1063
+ out = [(f"{pad}if ({self._expr(stmt.condition)}) {{", span)]
1064
+ for child in stmt.body:
1065
+ out.extend(self._stmt(child, indent + 1))
1066
+ out.append((pad + "}", span))
1067
+ if stmt.otherwise:
1068
+ out[-1] = (out[-1][0] + " else {", span)
1069
+ for child in stmt.otherwise:
1070
+ out.extend(self._stmt(child, indent + 1))
1071
+ out.append((pad + "}", span))
1072
+ return out
1073
+ if isinstance(stmt, IRWhile):
1074
+ out = [(f"{pad}while ({self._expr(stmt.condition)}) {{", span)]
1075
+ for child in stmt.body:
1076
+ out.extend(self._stmt(child, indent + 1))
1077
+ out.append((pad + "}", span))
1078
+ return out
1079
+ if isinstance(stmt, IRRangeLoop):
1080
+ start = self._temp_name(stmt.start_temp, "range_start")
1081
+ stop = self._temp_name(stmt.stop_temp, "range_stop")
1082
+ step = self._temp_name(stmt.step_temp, "range_step")
1083
+ iterator = self._temp_name(stmt.iterator_temp, "range_iter")
1084
+ out = [
1085
+ (f"{pad}int32_t {start} = {self._expr(stmt.start)};", span),
1086
+ (f"{pad}int32_t {stop} = {self._expr(stmt.stop)};", span),
1087
+ (f"{pad}int32_t {step} = {self._expr(stmt.step)};", span),
1088
+ ]
1089
+ step_value = self._const_int(stmt.step)
1090
+ if step_value == 0:
1091
+ raise InternalCompilerError("A zero range step reached the Uno backend after validation.")
1092
+ if step_value is None:
1093
+ out.append((f"{pad}if ({step} == 0) _bj_numeric_fail();", span))
1094
+ cond = f"({step} > 0 ? {iterator} < {stop} : {iterator} > {stop})"
1095
+ else:
1096
+ cond = f"{iterator} {'<' if step_value > 0 else '>'} {stop}"
1097
+ # Python range() never computes a value past the final element, but a C++
1098
+ # for-loop does evaluate `iterator + step` once more. Near INT32_MAX/MIN
1099
+ # that final increment must not be an Int32 overflow trap (or UB), so the
1100
+ # iterator is widened unless the whole progression is statically in range.
1101
+ bounds = self._constant_range_bounds(stmt)
1102
+ iterator_type = "int64_t"
1103
+ if bounds is not None and step_value is not None:
1104
+ after_last = (bounds[1] + step_value) if step_value > 0 else (bounds[0] + step_value)
1105
+ if -(2**31) <= after_last <= 2**31 - 1:
1106
+ iterator_type = "int32_t"
1107
+ elif (
1108
+ step_value is not None
1109
+ and self._const_int(stmt.start) is not None
1110
+ and self._const_int(stmt.stop) is not None
1111
+ ):
1112
+ # Constant but empty progression: the body never runs.
1113
+ iterator_type = "int32_t"
1114
+ out.append((f"{pad}for ({iterator_type} {iterator} = {start}; {cond}; {iterator} += {step}) {{", span))
1115
+ narrow = "(int32_t)" if iterator_type == "int64_t" else ""
1116
+ out.append((f"{pad} {stmt.target.backend_name} = {narrow}{iterator};", span))
1117
+ previous = self._active_int_ranges.get(stmt.target.symbol_id)
1118
+ facts = self._constant_range_bounds(stmt)
1119
+ if facts is not None and not self._body_writes(stmt.body, stmt.target.symbol_id, stmt.target.kind != SymbolKind.LOCAL):
1120
+ self._active_int_ranges[stmt.target.symbol_id] = facts
1121
+ else:
1122
+ # The loop variable is reassigned in the body: its range is not proven.
1123
+ self._active_int_ranges.pop(stmt.target.symbol_id, None)
1124
+ try:
1125
+ for child in stmt.body:
1126
+ out.extend(self._stmt(child, indent + 1))
1127
+ finally:
1128
+ if previous is None:
1129
+ self._active_int_ranges.pop(stmt.target.symbol_id, None)
1130
+ else:
1131
+ self._active_int_ranges[stmt.target.symbol_id] = previous
1132
+ out.append((pad + "}", span))
1133
+ return out
1134
+ if isinstance(stmt, IRFixedListLoop):
1135
+ typ = stmt.collection.type
1136
+ if typ.kind != TypeKind.FIXED_LIST or typ.length is None:
1137
+ raise InternalCompilerError("Fixed-list loop reached Uno backend without fixed-list type.")
1138
+ iterator = self._temp_name(stmt.iterator_temp, "list_iter")
1139
+ out: list[tuple[str, SourceSpan | None]] = []
1140
+ start_name: str | None = None
1141
+ if stmt.index_start is not None:
1142
+ if stmt.index_start_temp is None:
1143
+ raise InternalCompilerError("enumerate(start) IR is missing its evaluate-once temporary.")
1144
+ start_name = self._temp_name(stmt.index_start_temp, "enumerate_start")
1145
+ out.append((f"{pad}int32_t {start_name} = {self._expr(stmt.index_start)};", span))
1146
+ out.append((f"{pad}for (int32_t {iterator} = 0; {iterator} < {typ.length}; ++{iterator}) {{", span))
1147
+ if stmt.index_target is not None:
1148
+ if start_name is None:
1149
+ out.append((f"{pad} {stmt.index_target.backend_name} = {iterator};", span))
1150
+ else:
1151
+ out.append((f"{pad} {stmt.index_target.backend_name} = _bj_i32_add({start_name}, {iterator});", span))
1152
+ out.append((f"{pad} {stmt.value_target.backend_name} = {stmt.collection.backend_name}[{iterator}];", span))
1153
+ for child in stmt.body:
1154
+ out.extend(self._stmt(child, indent + 1))
1155
+ out.append((pad + "}", span))
1156
+ return out
1157
+ raise InternalCompilerError(f"Unsupported typed IR statement {type(stmt).__name__} reached Uno backend.")
1158
+
1159
+ def _expr(self, expr: IRExpr) -> str:
1160
+ if isinstance(expr, IRConstant):
1161
+ if expr.value is True: return "true"
1162
+ if expr.value is False: return "false"
1163
+ if isinstance(expr.value, str): return self._cpp_string_literal(expr.value)
1164
+ return repr(expr.value)
1165
+ if isinstance(expr, IRLoad):
1166
+ if expr.symbol.kind in {SymbolKind.API, SymbolKind.BUILTIN}:
1167
+ return expr.symbol.source_name
1168
+ return expr.symbol.backend_name
1169
+ if isinstance(expr, IRListLiteral):
1170
+ return "{" + ", ".join(self._expr(x) for x in expr.elements) + "}"
1171
+ if isinstance(expr, IRIndex):
1172
+ if not isinstance(expr.collection, IRLoad):
1173
+ raise InternalCompilerError("Fixed-list IR index has no direct list load.")
1174
+ return f"{self._expr(expr.collection)}[{self._list_index_expr(expr.index, expr.collection.type)}]"
1175
+ if isinstance(expr, IRUnary):
1176
+ operand = self._expr(expr.operand)
1177
+ if expr.op == IRUnaryOp.NOT:
1178
+ return f"(!{operand})"
1179
+ if isinstance(expr.operand, IRConstant) and expr.operand.value == 2**31:
1180
+ return "(-2147483647L - 1L)"
1181
+ if expr.type.kind == TypeKind.INT and IREffect.MAY_FAIL in expr.effects:
1182
+ return f"_bj_i32_neg({operand})"
1183
+ return f"(-{operand})"
1184
+ if isinstance(expr, IRBinary):
1185
+ return self._binary_expr(expr)
1186
+ if isinstance(expr, IRCompareChain):
1187
+ return self._comparison_expr(expr)
1188
+ if isinstance(expr, IRBool):
1189
+ op = " && " if expr.op == IRBoolOp.AND else " || "
1190
+ return "(" + op.join(self._expr(x) for x in expr.values) + ")"
1191
+ if isinstance(expr, IRConditional):
1192
+ return f"({self._expr(expr.condition)} ? {self._expr(expr.when_true)} : {self._expr(expr.when_false)})"
1193
+ if isinstance(expr, IRCall):
1194
+ return self._call(expr)
1195
+ if isinstance(expr, IRFormatString):
1196
+ raise InternalCompilerError("Output-only f-string reached scalar expression generation.")
1197
+ raise InternalCompilerError(f"Unsupported typed IR expression {type(expr).__name__} reached Uno backend.")
1198
+
1199
+ def _binary_expr(self, expr: IRBinary) -> str:
1200
+ left, right = self._expr(expr.left), self._expr(expr.right)
1201
+ def operation(a: str, b: str) -> str:
1202
+ if expr.type.kind == TypeKind.TIME_U32 and expr.op == IRBinaryOp.SUBTRACT:
1203
+ return f"((uint32_t)({a}) - (uint32_t)({b}))"
1204
+ if expr.op == IRBinaryOp.TRUE_DIVIDE:
1205
+ return f"_bj_true_div_f32((float)({a}), (float)({b}))"
1206
+ if expr.op == IRBinaryOp.MODULO:
1207
+ return f"_bj_mod_f32((float)({a}), (float)({b}))" if expr.type.kind == TypeKind.FLOAT else f"_bj_mod_i32({a}, {b})"
1208
+ if expr.type.kind == TypeKind.INT and expr.checked:
1209
+ helper = {IRBinaryOp.ADD:"_bj_i32_add", IRBinaryOp.SUBTRACT:"_bj_i32_sub", IRBinaryOp.MULTIPLY:"_bj_i32_mul"}.get(expr.op)
1210
+ if helper is not None: return f"{helper}({a}, {b})"
1211
+ op = {IRBinaryOp.ADD:"+", IRBinaryOp.SUBTRACT:"-", IRBinaryOp.MULTIPLY:"*"}.get(expr.op)
1212
+ if op is None: raise InternalCompilerError("Unknown binary IR operation reached backend.")
1213
+ return f"({a} {op} {b})"
1214
+ if self._expr_has_effects(expr.left) or self._expr_has_effects(expr.right):
1215
+ ltmp = self._new_temp("lhs"); rtmp = self._new_temp("rhs")
1216
+ return f"([&]() -> {expr.type.cpp_type} {{ {expr.left.type.cpp_type} {ltmp} = {left}; {expr.right.type.cpp_type} {rtmp} = {right}; return {operation(ltmp, rtmp)}; }}())"
1217
+ if (
1218
+ expr.type.kind == TypeKind.INT
1219
+ and not expr.checked
1220
+ and expr.op in {IRBinaryOp.ADD, IRBinaryOp.SUBTRACT, IRBinaryOp.MULTIPLY}
1221
+ and not self._is_int32_storage_load(expr.left)
1222
+ and not self._is_int32_storage_load(expr.right)
1223
+ ):
1224
+ # AVR `int` is 16 bits: `1000 * 60` or `32767 + 29` written with C++ int
1225
+ # literals/Arduino macros would be computed (and overflow) in 16 bits even
1226
+ # though Barmaja proved the Python result fits Int32. Force Int32 arithmetic.
1227
+ left = f"(int32_t)({left})"
1228
+ return operation(left, right)
1229
+
1230
+ @staticmethod
1231
+ def _is_int32_storage_load(expr: IRExpr) -> bool:
1232
+ """True when the emitted C++ expression already has type int32_t."""
1233
+ if expr.type.kind != TypeKind.INT:
1234
+ return False
1235
+ if isinstance(expr, IRLoad):
1236
+ return expr.symbol.kind not in {SymbolKind.API, SymbolKind.BUILTIN}
1237
+ if isinstance(expr, IRBinary):
1238
+ # checked helpers return int32_t; unchecked ADD/SUB/MUL are widened here;
1239
+ # modulo uses _bj_mod_i32 (int32_t).
1240
+ return True
1241
+ if isinstance(expr, IRIndex):
1242
+ return True # element of an int32_t fixed-list array
1243
+ if isinstance(expr, IRCall) and expr.kind == IRCallKind.USER_FUNCTION:
1244
+ return True # user functions returning Int are declared int32_t
1245
+ return False
1246
+
1247
+ def _comparison_expr(self, expr: IRCompareChain) -> str:
1248
+ names = [self._temp_name(t, "cmp") for t in expr.operand_temps]
1249
+ pieces = [f"{expr.operands[0].type.cpp_type} {names[0]} = {self._expr(expr.operands[0])};"]
1250
+ for i, op in enumerate(expr.operators):
1251
+ right = expr.operands[i + 1]
1252
+ pieces.append(f"{right.type.cpp_type} {names[i+1]} = {self._expr(right)};")
1253
+ cmp_expr = self._comparison_operand(expr.operands[i], right, op, names[i], names[i+1])
1254
+ pieces.append(f"if (!({cmp_expr})) return false;")
1255
+ pieces.append("return true;")
1256
+ return "([&]() -> bool { " + " ".join(pieces) + " }())"
1257
+
1258
+ def _comparison_operand(self, left_expr: IRExpr, right_expr: IRExpr, op: IRCompareOp, left: str, right: str) -> str:
1259
+ lt, rt = left_expr.type.kind, right_expr.type.kind
1260
+ if lt == TypeKind.STRING and rt == TypeKind.STRING:
1261
+ eq = f"_bj_str_eq({left}, {right})"
1262
+ if op == IRCompareOp.EQ: return eq
1263
+ if op == IRCompareOp.NE: return f"(!{eq})"
1264
+ raise InternalCompilerError("Ordered string comparison reached backend after validation.")
1265
+ if lt == TypeKind.TIME_U32 and rt == TypeKind.INT:
1266
+ right = f"((uint32_t)({right}))" if self._known_nonnegative_int(right_expr) else f"_bj_time_threshold({right})"
1267
+ elif rt == TypeKind.TIME_U32 and lt == TypeKind.INT:
1268
+ left = f"((uint32_t)({left}))" if self._known_nonnegative_int(left_expr) else f"_bj_time_threshold({left})"
1269
+ token = {IRCompareOp.EQ:"==", IRCompareOp.NE:"!=", IRCompareOp.LT:"<", IRCompareOp.LE:"<=", IRCompareOp.GT:">", IRCompareOp.GE:">="}[op]
1270
+ return f"({left} {token} {right})"
1271
+
1272
+ def _call(self, call: IRCall) -> str:
1273
+ args = [self._expr(a) for a in call.args]
1274
+ if call.kind == IRCallKind.BUILTIN:
1275
+ name = call.name
1276
+ if name == "round":
1277
+ arg = call.args[0]
1278
+ if len(args) == 1:
1279
+ return args[0] if arg.type.kind == TypeKind.INT else f"_bj_round_i32((float)({args[0]}))"
1280
+ ndigits = self._const_int(call.args[1])
1281
+ if ndigits is None: raise InternalCompilerError("round() ndigits is not constant in typed IR.")
1282
+ return f"_bj_round_i32_digits({args[0]}, {ndigits})" if arg.type.kind == TypeKind.INT else f"_bj_round_f32_digits((float)({args[0]}), {ndigits})"
1283
+ if name in {"min", "max"}: return self._minmax_expr(call, want_min=name == "min")
1284
+ if name == "int": return f"_bj_float_to_i32((float)({args[0]}))" if call.args[0].type.kind == TypeKind.FLOAT else f"((int32_t)({args[0]}))"
1285
+ if name == "float": return f"((float)({args[0]}))"
1286
+ if name == "bool": return f"((bool)({args[0]}))"
1287
+ if name == "abs": return f"_bj_i32_abs({args[0]})" if call.args[0].type.kind == TypeKind.INT else f"fabsf((float)({args[0]}))"
1288
+ if name == "len":
1289
+ typ = call.args[0].type
1290
+ if typ.kind != TypeKind.FIXED_LIST or typ.length is None: raise InternalCompilerError("len() reached backend without fixed-list IR type.")
1291
+ return str(typ.length)
1292
+ if name == "sum":
1293
+ arg = call.args[0]
1294
+ if not isinstance(arg, IRLoad) or arg.type.kind != TypeKind.FIXED_LIST or arg.type.length is None or arg.type.element_type is None:
1295
+ raise InternalCompilerError("sum() reached backend without fixed-list load.")
1296
+ idx = self._new_temp("sum_i"); acc = self._new_temp("sum_acc")
1297
+ if arg.type.element_type.kind == TypeKind.INT:
1298
+ return f"([&]() -> int32_t {{ int32_t {acc} = 0; for (int32_t {idx} = 0; {idx} < {arg.type.length}; ++{idx}) {acc} = _bj_i32_add({acc}, {arg.symbol.backend_name}[{idx}]); return {acc}; }}())"
1299
+ return f"([&]() -> float {{ float {acc} = 0.0f; for (int32_t {idx} = 0; {idx} < {arg.type.length}; ++{idx}) {acc} += {arg.symbol.backend_name}[{idx}]; return {acc}; }}())"
1300
+ raise InternalCompilerError(f"Unsupported builtin '{name}' reached Uno backend.")
1301
+
1302
+ if call.kind == IRCallKind.USER_FUNCTION:
1303
+ if call.function_symbol is None: raise InternalCompilerError("User-function IR call is missing function symbol.")
1304
+ callee = call.function_symbol.backend_name
1305
+ elif call.kind == IRCallKind.API_FUNCTION:
1306
+ callee = call.name
1307
+ if call.name == "tone":
1308
+ if not self._known_int_within(call.args[1], 31, 65535):
1309
+ args[1] = f"_bj_tone_frequency({args[1]})"; self._dynamic_helpers.add("_bj_tone_frequency")
1310
+ if len(call.args) == 3 and self._needs_nonnegative_guard(call.args[2]):
1311
+ args[2] = f"_bj_time_threshold({args[2]})"; self._dynamic_helpers.add("_bj_time_threshold")
1312
+ elif call.name == "delayMicroseconds":
1313
+ if not self._known_int_within(call.args[0], 0, 16383):
1314
+ args[0] = f"_bj_delay_us({args[0]})"; self._dynamic_helpers.add("_bj_delay_us")
1315
+ elif call.name == "map":
1316
+ callee = "_bj_arduino_map"; self._dynamic_helpers.add("_bj_arduino_map")
1317
+ elif call.name == "constrain":
1318
+ callee = "_bj_constrain_int"; self._dynamic_helpers.add("_bj_constrain_int")
1319
+ if call.name == "analogWrite" and len(call.args) >= 2 and id(call) not in self._safe_pwm_call_ids and not self._known_pwm_safe(call.args[1], self._active_int_ranges):
1320
+ args[1] = f"_bj_pwm_value({args[1]})"; self._dynamic_helpers.add("_bj_pwm_value")
1321
+ if call.name == "pinMode" and len(call.args) >= 2 and not self._known_int_within(call.args[1], 0, 2):
1322
+ args[1] = f"_bj_pin_mode({args[1]})"; self._dynamic_helpers.add("_bj_pin_mode")
1323
+ if call.name == "digitalWrite" and len(call.args) >= 2 and not self._known_int_within(call.args[1], 0, 1):
1324
+ args[1] = f"_bj_digital_level({args[1]})"; self._dynamic_helpers.add("_bj_digital_level")
1325
+ pin_guard = _PIN_GUARDS.get(call.name)
1326
+ if pin_guard is not None and call.args and not self._pin_is_proven(call.args[0], _PIN_SETS[pin_guard]):
1327
+ args[0] = f"{pin_guard}({args[0]})"; self._dynamic_helpers.add(pin_guard)
1328
+ if call.name == "delay" and call.args and self._needs_nonnegative_guard(call.args[0]):
1329
+ args[0] = f"_bj_time_threshold({args[0]})"; self._dynamic_helpers.add("_bj_time_threshold")
1330
+ elif call.kind == IRCallKind.OBJECT_METHOD:
1331
+ if call.owner_symbol is None or call.method_name is None: raise InternalCompilerError("Object-method IR call is incomplete.")
1332
+ callee = f"{call.owner_symbol.backend_name}.{call.method_name}"
1333
+ guard = self._object_arg_guard(call)
1334
+ if guard is not None:
1335
+ args[0] = f"{guard}({args[0]})"; self._dynamic_helpers.add(guard)
1336
+ cls = call.owner_symbol.type.hardware_class
1337
+ if cls == "Servo" and call.method_name == "writeMicroseconds" and call.args:
1338
+ if not self._known_int_within(call.args[0], 0, 3000):
1339
+ args[0] = f"_bj_servo_pulse({args[0]})"; self._dynamic_helpers.add("_bj_servo_pulse")
1340
+ if cls == "Servo" and call.method_name == "attach" and len(call.args) == 3:
1341
+ return self._ordered_servo_attach(call, callee, args)
1342
+ if cls == "LiquidCrystal_I2C" and call.method_name == "begin" and len(call.args) >= 2:
1343
+ for index in (0, 1):
1344
+ if not self._known_int_within(call.args[index], 1, 255):
1345
+ args[index] = f"_bj_lcd_dimension({args[index]})"; self._dynamic_helpers.add("_bj_lcd_dimension")
1346
+ if len(call.args) >= 3 and not self._known_int_in_set(call.args[2], {0, 4}):
1347
+ args[2] = f"_bj_lcd_charsize({args[2]})"; self._dynamic_helpers.add("_bj_lcd_charsize")
1348
+ if cls == "LiquidCrystal_I2C" and call.method_name == "setCursor" and len(call.args) >= 2:
1349
+ if not self._known_int_within(call.args[0], 0, 255):
1350
+ args[0] = f"_bj_lcd_cursor_column({args[0]})"; self._dynamic_helpers.add("_bj_lcd_cursor_column")
1351
+ if not self._known_int_within(call.args[1], 0, 3):
1352
+ args[1] = f"_bj_lcd_cursor_row({args[1]})"; self._dynamic_helpers.add("_bj_lcd_cursor_row")
1353
+ elif call.kind == IRCallKind.SINGLETON_METHOD:
1354
+ if call.owner_name is None or call.method_name is None: raise InternalCompilerError("Singleton-method IR call is incomplete.")
1355
+ callee = f"{call.owner_name}.{call.method_name}"
1356
+ if call.owner_name == "Wire":
1357
+ if call.method_name in {"beginTransmission", "requestFrom"} and call.args and not self._known_int_within(call.args[0], 0, 127):
1358
+ args[0] = f"_bj_i2c_address({args[0]})"; self._dynamic_helpers.add("_bj_i2c_address")
1359
+ if call.method_name == "write" and call.args and not self._known_int_within(call.args[0], 0, 255):
1360
+ args[0] = f"_bj_byte_value({args[0]})"; self._dynamic_helpers.add("_bj_byte_value")
1361
+ if call.method_name == "requestFrom" and len(call.args) >= 2 and not self._known_int_within(call.args[1], 0, 255):
1362
+ args[1] = f"_bj_wire_count({args[1]})"; self._dynamic_helpers.add("_bj_wire_count")
1363
+ # Pinned AVR TwoWire overloads beginTransmission(uint8_t|int) and
1364
+ # requestFrom(uint8_t,uint8_t|int,int): an int32_t argument (any Python
1365
+ # variable) is ambiguous in C++. The values are already proven or guarded
1366
+ # to 0..127 / 0..255, so select the uint8_t overloads explicitly.
1367
+ # The cast is applied at the call itself (also to ordered argument
1368
+ # temporaries, which are declared int32_t).
1369
+ if call.method_name == "beginTransmission" and call.args:
1370
+ return self._ordered_call(call, callee, args, casts={0: "uint8_t"})
1371
+ if call.method_name == "requestFrom" and len(call.args) >= 2:
1372
+ return self._ordered_call(call, callee, args, casts={0: "uint8_t", 1: "uint8_t"})
1373
+ if call.owner_name == "Serial" and call.method_name in {"print", "println"} and len(call.args) == 2:
1374
+ if not self._known_int_within(call.args[1], 0, 255):
1375
+ args[1] = f"_bj_arduino_format({args[1]})"; self._dynamic_helpers.add("_bj_arduino_format")
1376
+ else:
1377
+ raise InternalCompilerError("Unknown IR call category reached Uno backend.")
1378
+ return self._ordered_call(call, callee, args)
1379
+
1380
+ def _pin_is_proven(self, expr: IRExpr, allowed: frozenset[int]) -> bool:
1381
+ """True only when the pin value is fixed at compile time (or range-proven) and valid.
1382
+
1383
+ Target analysis rejects provably wrong pins; everything it cannot fix at
1384
+ compile time gets a runtime check so an invalid pin never reaches the core.
1385
+ """
1386
+ value = self._const_int(expr)
1387
+ if value is not None:
1388
+ return value in allowed
1389
+ if isinstance(expr, IRLoad):
1390
+ if expr.symbol.kind in {SymbolKind.API, SymbolKind.BUILTIN}:
1391
+ from ..api_registry import CONSTANT_VALUES
1392
+ return CONSTANT_VALUES.get(expr.symbol.source_name) in allowed
1393
+ if expr.symbol.symbol_id in self._constant_symbol_values:
1394
+ return self._constant_symbol_values[expr.symbol.symbol_id] in allowed
1395
+ facts = self._active_int_ranges.get(expr.symbol.symbol_id)
1396
+ if facts is not None:
1397
+ return all(v in allowed for v in range(facts[0], facts[1] + 1))
1398
+ return False
1399
+
1400
+ def _needs_nonnegative_guard(self, expr: IRExpr) -> bool:
1401
+ value = self._const_int(expr)
1402
+ if value is not None:
1403
+ return value < 0
1404
+ if isinstance(expr, IRLoad):
1405
+ facts = self._active_int_ranges.get(expr.symbol.symbol_id)
1406
+ if facts is not None and facts[0] >= 0:
1407
+ return False
1408
+ return True
1409
+
1410
+ def _object_arg_guard(self, call: IRCall) -> str | None:
1411
+ """Runtime argument policy for hardware methods (registry constraints)."""
1412
+ if not call.args or call.owner_symbol is None:
1413
+ return None
1414
+ cls = call.owner_symbol.type.hardware_class
1415
+ if cls == "Servo" and call.method_name == "write":
1416
+ value = self._const_int(call.args[0])
1417
+ if value is not None and 0 <= value <= 180:
1418
+ return None
1419
+ if isinstance(call.args[0], IRLoad):
1420
+ facts = self._active_int_ranges.get(call.args[0].symbol.symbol_id)
1421
+ if facts is not None and facts[0] >= 0 and facts[1] <= 180:
1422
+ return None
1423
+ return "_bj_servo_angle"
1424
+ if cls == "Ultrasonic" and call.method_name == "setTimeout":
1425
+ return "_bj_time_threshold" if self._needs_nonnegative_guard(call.args[0]) else None
1426
+ return None
1427
+
1428
+ def _ordered_servo_attach(self, call: IRCall, callee: str, args: list[str]) -> str:
1429
+ # Servo 1.3.0 stores min/max calibration offsets in int8_t. Materialize
1430
+ # all Python arguments in order, validate the pair once, then call the library.
1431
+ self._dynamic_helpers.add("_bj_servo_attach_bounds")
1432
+ decls: list[str] = []; names: list[str] = []
1433
+ for arg, text in zip(call.args, args):
1434
+ name = self._new_temp("servo_attach_arg"); names.append(name)
1435
+ decls.append(f"{arg.type.cpp_type} {name} = {text};")
1436
+ decls.append(f"_bj_servo_attach_bounds({names[1]}, {names[2]});")
1437
+ invocation = f"{callee}({', '.join(names)})"
1438
+ if call.type.kind == TypeKind.VOID:
1439
+ return "([&]() { " + " ".join(decls) + f" {invocation}; }}())"
1440
+ return f"([&]() -> {call.type.cpp_type} {{ " + " ".join(decls) + f" return {invocation}; }}())"
1441
+
1442
+ def _ordered_call(self, call: IRCall, callee: str, args: list[str], *, casts: dict[int, str] | None = None) -> str:
1443
+ casts = casts or {}
1444
+ def cast(index: int, text: str) -> str:
1445
+ return f"({casts[index]})({text})" if index in casts else text
1446
+ if len(call.args) <= 1 or not any(self._expr_has_effects(a) for a in call.args):
1447
+ return f"{callee}({', '.join(cast(i, a) for i, a in enumerate(args))})"
1448
+ decls: list[str] = []; names: list[str] = []
1449
+ for arg, text in zip(call.args, args):
1450
+ name = self._new_temp("arg"); names.append(name); decls.append(f"{arg.type.cpp_type} {name} = {text};")
1451
+ invocation = f"{callee}({', '.join(cast(i, n) for i, n in enumerate(names))})"
1452
+ if call.type.kind == TypeKind.VOID: return "([&]() { " + " ".join(decls) + f" {invocation}; }}())"
1453
+ return f"([&]() -> {call.type.cpp_type} {{ " + " ".join(decls) + f" return {invocation}; }}())"
1454
+
1455
+ def _minmax_expr(self, call: IRCall, *, want_min: bool) -> str:
1456
+ names: list[str] = []; parts: list[str] = []
1457
+ for arg in call.args:
1458
+ name = self._new_temp("minmax_arg"); names.append(name); parts.append(f"{arg.type.cpp_type} {name} = {self._expr(arg)};")
1459
+ result = self._new_temp("minmax"); parts.append(f"{call.type.cpp_type} {result} = {names[0]};")
1460
+ token = "<" if want_min else ">"
1461
+ for name in names[1:]: parts.append(f"if ({name} {token} {result}) {result} = {name};")
1462
+ parts.append(f"return {result};")
1463
+ return f"([&]() -> {call.type.cpp_type} {{ " + " ".join(parts) + " }())"
1464
+
1465
+ def _python_print(self, stmt: IRPrint, indent: int) -> list[str]:
1466
+ pad = " " * indent; preludes: list[str] = []; parts: list[_OutputPart] = []
1467
+ for i, arg in enumerate(stmt.values):
1468
+ if i: parts.append(_OutputPart(literal=" "))
1469
+ pre, argparts = self._prepare_output_arg(arg); preludes.extend(pre); parts.extend(argparts)
1470
+ out = [pad + x for x in preludes]
1471
+ for part in parts: out.extend(self._emit_output_part("Serial", part, indent, lcd_mode=False))
1472
+ if stmt.newline: out.append(pad + "Serial.println();")
1473
+ return out
1474
+
1475
+ def _explicit_output_stmt(self, expr: IRExpr, indent: int) -> list[str] | None:
1476
+ if not isinstance(expr, IRCall) or expr.kind not in {IRCallKind.OBJECT_METHOD, IRCallKind.SINGLETON_METHOD}: return None
1477
+ if expr.method_name not in {"print", "println"} or len(expr.args) != 1: return None
1478
+ owner = expr.owner_symbol.backend_name if expr.owner_symbol is not None else expr.owner_name
1479
+ if owner is None: return None
1480
+ lcd_mode = expr.owner_symbol is not None and expr.owner_symbol.type.hardware_class == "LiquidCrystal_I2C"
1481
+ arg = expr.args[0]; pad = " " * indent
1482
+ if isinstance(arg, IRConstant) and isinstance(arg.value, str): return [f"{pad}{owner}.{expr.method_name}({self._flash_literal(arg.value, lcd_mode=lcd_mode)});"]
1483
+ if isinstance(arg, IRFormatString):
1484
+ preludes, parts = self._prepare_output_arg(arg); out = [pad + x for x in preludes]
1485
+ for part in parts: out.extend(self._emit_output_part(owner, part, indent, lcd_mode=lcd_mode))
1486
+ if expr.method_name == "println": out.append(f"{pad}{owner}.println();")
1487
+ return out
1488
+ return None
1489
+
1490
+ def _prepare_output_arg(self, expr: IRExpr) -> tuple[list[str], list[_OutputPart]]:
1491
+ if isinstance(expr, IRFormatString):
1492
+ preludes: list[str] = []; parts: list[_OutputPart] = []
1493
+ for part in expr.parts:
1494
+ if isinstance(part, str):
1495
+ if part: parts.append(_OutputPart(literal=part))
1496
+ else:
1497
+ pre, out = self._prepare_output_value(part.value, part.precision); preludes.extend(pre); parts.append(out)
1498
+ return preludes, parts
1499
+ if isinstance(expr, IRConstant) and isinstance(expr.value, str): return [], [_OutputPart(literal=expr.value)]
1500
+ pre, part = self._prepare_output_value(expr, None); return pre, [part]
1501
+
1502
+ def _prepare_output_value(self, expr: IRExpr, precision: int | None) -> tuple[list[str], _OutputPart]:
1503
+ if expr.type.kind not in {TypeKind.BOOL, TypeKind.INT, TypeKind.TIME_U32, TypeKind.FLOAT, TypeKind.STRING}: raise InternalCompilerError(f"Non-printable IR type {expr.type.kind.value} reached output backend.")
1504
+ text = self._expr(expr)
1505
+ if isinstance(expr, IRConstant): return [], _OutputPart(expression=text, type_kind=expr.type.kind, precision=precision)
1506
+ temp = self._new_temp("print_arg")
1507
+ return [f"{expr.type.cpp_type} {temp} = {text};"], _OutputPart(expression=temp, type_kind=expr.type.kind, precision=precision)
1508
+
1509
+ def _emit_output_part(self, sink: str, part: _OutputPart, indent: int, *, lcd_mode: bool) -> list[str]:
1510
+ pad = " " * indent
1511
+ if part.literal is not None: return [f"{pad}{sink}.print({self._flash_literal(part.literal, lcd_mode=lcd_mode)});"]
1512
+ if part.expression is None or part.type_kind is None: raise InternalCompilerError("Malformed output part reached Uno backend.")
1513
+ e = part.expression
1514
+ if part.precision is not None: return [f"{pad}_bj_print_py_fixed({sink}, (float)({e}), {part.precision});"]
1515
+ if part.type_kind == TypeKind.BOOL: return [f"{pad}if ({e}) {sink}.print(F(\"True\")); else {sink}.print(F(\"False\"));"]
1516
+ if part.type_kind == TypeKind.INT: return [f"{pad}{sink}.print((int32_t)({e}));"]
1517
+ if part.type_kind == TypeKind.TIME_U32: return [f"{pad}{sink}.print((uint32_t)({e}));"]
1518
+ if part.type_kind == TypeKind.FLOAT: return [f"{pad}_bj_print_py_float({sink}, (float)({e}));"]
1519
+ if part.type_kind == TypeKind.STRING: return [f"{pad}{sink}.print({e});"]
1520
+ raise InternalCompilerError("Unsupported output type reached Uno backend.")
1521
+
1522
+ def _required_numeric_helpers(self) -> set[str]:
1523
+ need: set[str] = set()
1524
+ for item in _walk(self.ir):
1525
+ if isinstance(item, IRIndex) and self._const_int(item.index) is None: need.add("_bj_list_index")
1526
+ elif isinstance(item, IRIndexStore) and self._const_int(item.index) is None: need.add("_bj_list_index")
1527
+ elif isinstance(item, IRRangeLoop):
1528
+ # The range iterator no longer uses checked addition (see _stmt); only a
1529
+ # dynamic step needs the zero-step trap.
1530
+ if self._const_int(item.step) is None: need.add("_bj_numeric_fail")
1531
+ elif isinstance(item, IRFixedListLoop) and item.index_start is not None: need.add("_bj_i32_add")
1532
+ elif isinstance(item, IRBinary):
1533
+ if item.op == IRBinaryOp.TRUE_DIVIDE: need.add("_bj_true_div_f32")
1534
+ elif item.op == IRBinaryOp.MODULO: need.add("_bj_mod_f32" if item.type.kind == TypeKind.FLOAT else "_bj_mod_i32")
1535
+ elif item.checked and item.type.kind == TypeKind.INT:
1536
+ name = {IRBinaryOp.ADD:"_bj_i32_add", IRBinaryOp.SUBTRACT:"_bj_i32_sub", IRBinaryOp.MULTIPLY:"_bj_i32_mul"}.get(item.op)
1537
+ if name: need.add(name)
1538
+ elif isinstance(item, IRAugStore):
1539
+ if item.op == IRBinaryOp.TRUE_DIVIDE: need.add("_bj_true_div_f32")
1540
+ elif item.op == IRBinaryOp.MODULO: need.add("_bj_mod_f32" if item.target.type.kind == TypeKind.FLOAT else "_bj_mod_i32")
1541
+ elif item.target.type.kind == TypeKind.INT:
1542
+ name = {IRBinaryOp.ADD:"_bj_i32_add", IRBinaryOp.SUBTRACT:"_bj_i32_sub", IRBinaryOp.MULTIPLY:"_bj_i32_mul"}.get(item.op)
1543
+ if name: need.add(name)
1544
+ elif isinstance(item, IRUnary) and item.type.kind == TypeKind.INT and IREffect.MAY_FAIL in item.effects: need.add("_bj_i32_neg")
1545
+ elif isinstance(item, IRCompareChain):
1546
+ for a, b in zip(item.operands, item.operands[1:]):
1547
+ if a.type.kind == b.type.kind == TypeKind.STRING: need.add("_bj_str_eq")
1548
+ if (a.type.kind == TypeKind.TIME_U32 and b.type.kind == TypeKind.INT and not self._known_nonnegative_int(b)) or (b.type.kind == TypeKind.TIME_U32 and a.type.kind == TypeKind.INT and not self._known_nonnegative_int(a)): need.add("_bj_time_threshold")
1549
+ elif isinstance(item, IRCall):
1550
+ if item.kind == IRCallKind.API_FUNCTION and item.name == "analogWrite" and len(item.args) >= 2 and id(item) not in self._safe_pwm_call_ids and not self._known_pwm_safe(item.args[1]): need.add("_bj_pwm_value")
1551
+ if item.kind == IRCallKind.BUILTIN:
1552
+ if item.name == "int" and item.args and item.args[0].type.kind == TypeKind.FLOAT: need.add("_bj_float_to_i32")
1553
+ elif item.name == "round" and item.args:
1554
+ if len(item.args) == 1 and item.args[0].type.kind == TypeKind.FLOAT: need.add("_bj_round_i32")
1555
+ elif len(item.args) == 2 and item.args[0].type.kind == TypeKind.FLOAT: need.add("_bj_round_f32_digits")
1556
+ elif len(item.args) == 2 and item.args[0].type.kind == TypeKind.INT: need.add("_bj_round_i32_digits")
1557
+ elif item.name == "abs" and item.args and item.args[0].type.kind == TypeKind.INT: need.add("_bj_i32_abs")
1558
+ elif item.name == "sum" and item.args and item.args[0].type.kind == TypeKind.FIXED_LIST and item.args[0].type.element_type is not None and item.args[0].type.element_type.kind == TypeKind.INT: need.add("_bj_i32_add")
1559
+ return _helper_closure(need)
1560
+
1561
+ @staticmethod
1562
+ def _helper_order(names: set[str]) -> list[str]:
1563
+ order = [_helper_name(line) for line in _NUMERIC_HELPER_BLOB.splitlines() if line.startswith("static ")]
1564
+ return [name for name in order if name in names]
1565
+
1566
+ def _needs_python_float_formatter(self) -> bool:
1567
+ for item in _walk(self.ir):
1568
+ if isinstance(item, IRPrint):
1569
+ if any(self._output_arg_needs_float_formatter(arg) for arg in item.values): return True
1570
+ elif isinstance(item, IRExprStmt) and isinstance(item.expression, IRCall):
1571
+ call = item.expression
1572
+ if call.method_name in {"print", "println"} and len(call.args) == 1 and isinstance(call.args[0], IRFormatString) and self._output_arg_needs_float_formatter(call.args[0]): return True
1573
+ return False
1574
+
1575
+ def _needs_fixed_float_formatter(self) -> bool:
1576
+ return any(isinstance(item, IRFormattedValue) and item.precision is not None for item in _walk(self.ir))
1577
+
1578
+ @staticmethod
1579
+ def _output_arg_needs_float_formatter(expr: IRExpr) -> bool:
1580
+ if isinstance(expr, IRFormatString): return any(isinstance(p, IRFormattedValue) and p.precision is None and p.value.type.kind == TypeKind.FLOAT for p in expr.parts)
1581
+ return expr.type.kind == TypeKind.FLOAT
1582
+
1583
+ @staticmethod
1584
+ def _expr_has_effects(expr: IRExpr) -> bool: return bool(expr.effects)
1585
+
1586
+ @staticmethod
1587
+ def _const_int(expr: IRExpr) -> int | None:
1588
+ if isinstance(expr, IRConstant) and isinstance(expr.value, int) and not isinstance(expr.value, bool): return expr.value
1589
+ if isinstance(expr, IRUnary) and expr.op == IRUnaryOp.NEGATE and expr.type.kind == TypeKind.INT:
1590
+ # Nested literal negation (e.g. round(x, --2)) is folded exactly like the
1591
+ # type/numeric phases fold it, so accepted programs never reach an ICE.
1592
+ inner = UnoCppBackend._const_int(expr.operand)
1593
+ return -inner if inner is not None else None
1594
+ return None
1595
+
1596
+ def _known_nonnegative_int(self, expr: IRExpr) -> bool:
1597
+ value = self._const_int(expr); return value is not None and value >= 0
1598
+
1599
+ def _known_int_within(self, expr: IRExpr, low: int, high: int) -> bool:
1600
+ value = self._const_int(expr)
1601
+ if value is not None:
1602
+ return low <= value <= high
1603
+ if isinstance(expr, IRLoad):
1604
+ if expr.symbol.kind in {SymbolKind.API, SymbolKind.BUILTIN}:
1605
+ from ..api_registry import CONSTANT_VALUES
1606
+ value = CONSTANT_VALUES.get(expr.symbol.source_name)
1607
+ return value is not None and low <= value <= high
1608
+ if expr.symbol.symbol_id in self._constant_symbol_values:
1609
+ value = self._constant_symbol_values[expr.symbol.symbol_id]
1610
+ return low <= value <= high
1611
+ facts = self._active_int_ranges.get(expr.symbol.symbol_id)
1612
+ return facts is not None and low <= facts[0] and facts[1] <= high
1613
+ return False
1614
+
1615
+ def _known_int_in_set(self, expr: IRExpr, allowed: set[int]) -> bool:
1616
+ value = self._const_int(expr)
1617
+ if value is not None:
1618
+ return value in allowed
1619
+ if isinstance(expr, IRLoad):
1620
+ if expr.symbol.kind in {SymbolKind.API, SymbolKind.BUILTIN}:
1621
+ from ..api_registry import CONSTANT_VALUES
1622
+ value = CONSTANT_VALUES.get(expr.symbol.source_name)
1623
+ return value in allowed if value is not None else False
1624
+ if expr.symbol.symbol_id in self._constant_symbol_values:
1625
+ return self._constant_symbol_values[expr.symbol.symbol_id] in allowed
1626
+ facts = self._active_int_ranges.get(expr.symbol.symbol_id)
1627
+ if facts is not None and facts[1] - facts[0] <= 16:
1628
+ return all(v in allowed for v in range(facts[0], facts[1] + 1))
1629
+ return False
1630
+
1631
+ def _known_pwm_safe(self, expr: IRExpr, ranges: dict[object, tuple[int, int]] | None = None) -> bool:
1632
+ value = self._const_int(expr)
1633
+ if value is not None:
1634
+ return 0 <= value <= 255
1635
+ if isinstance(expr, IRLoad):
1636
+ facts = (ranges or {}).get(expr.symbol.symbol_id)
1637
+ return facts is not None and 0 <= facts[0] and facts[1] <= 255
1638
+ return False
1639
+
1640
+ def _list_index_expr(self, index: IRExpr, list_type) -> str:
1641
+ if list_type.kind != TypeKind.FIXED_LIST or list_type.length is None: raise InternalCompilerError("List indexing reached backend without fixed-list type.")
1642
+ literal = self._const_int(index)
1643
+ return str(literal) if literal is not None else f"_bj_list_index({self._expr(index)}, {list_type.length})"
1644
+
1645
+ def _new_temp(self, hint: str) -> str:
1646
+ self._temp_counter += 1; return f"_bj_tmp_{hint}_{self._temp_counter:04d}"
1647
+
1648
+ def _temp_name(self, temp: IRTempRef, hint: str) -> str:
1649
+ name = self._temp_names.get(temp.temp_id)
1650
+ if name is None:
1651
+ name = self._new_temp(hint); self._temp_names[temp.temp_id] = name
1652
+ return name
1653
+
1654
+ @staticmethod
1655
+ def _flash_literal(text: str, *, lcd_mode: bool = False) -> str: return f"F({UnoCppBackend._cpp_string_literal(text, lcd_mode=lcd_mode)})"
1656
+
1657
+ @staticmethod
1658
+ def _cpp_string_literal(text: str, *, lcd_mode: bool = False) -> str:
1659
+ data = bytearray()
1660
+ if lcd_mode:
1661
+ for ch in text:
1662
+ code = ord(ch)
1663
+ if 0x20 <= code <= 0x7E: data.append(code)
1664
+ elif ch == "°": data.append(0xDF)
1665
+ else: raise InternalCompilerError("Unsupported LCD character reached backend after target validation.")
1666
+ else: data.extend(text.encode("utf-8"))
1667
+ pieces: list[str] = []
1668
+ for byte in data:
1669
+ if byte == 0x5C: pieces.append("\\\\")
1670
+ elif byte == 0x22: pieces.append('\\"')
1671
+ elif byte == 0x0A: pieces.append("\\n")
1672
+ elif byte == 0x0D: pieces.append("\\r")
1673
+ elif byte == 0x09: pieces.append("\\t")
1674
+ elif 0x20 <= byte <= 0x7E: pieces.append(chr(byte))
1675
+ else: pieces.append(f"\\{byte:03o}")
1676
+ return '"' + "".join(pieces) + '"'
1677
+
1678
+
1679
+ def generate_uno_cpp(ir_module: IRModule, target_plan: TargetPlan) -> GeneratedSketch:
1680
+ return UnoCppBackend(ir_module, target_plan).generate()