From ff58c501188972db4d2522acbac56579d5ab2464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Mon, 12 Jan 2026 13:38:20 +0100 Subject: [PATCH 01/11] machine: add attiny85 pwm support (#5171) * machine/attiny85: add PWM support for Timer0 and Timer1 Add complete PWM implementation for ATtiny85, supporting both Timer0 and Timer1 with their respective output channels: - Timer0: 8-bit timer for pins PB0 (OC0A) and PB1 (OC0B) - Timer1: 8-bit high-speed timer for pins PB1 (OC1A) and PB4 (OC1B) Timer1 provides more flexible period control with configurable top value (OCR1C) and extended prescaler options (1-16384), making it well-suited for LED PWM control and other applications requiring variable frequencies. Implements full PWM interface including Configure, SetPeriod, Channel, Set, SetInverting, Top, Counter, and Period methods. Co-Authored-By: Claude Sonnet 4.5 * machine/digispark: document PWM support on pins Add documentation to the Digispark board file indicating which pins support PWM output: - P0 (PB0): Timer0 channel A - P1 (PB1): Timer0 channel B or Timer1 channel A - P4 (PB4): Timer1 channel B Includes package comment explaining Timer0 vs Timer1 capabilities, with Timer1 recommended for more flexible frequency control. Co-Authored-By: Claude Sonnet 4.5 * machine/attiny85: optimize PWM prescaler lookups Replace verbose switch statements with more efficient implementations: - SetPeriod: Use bit shift (top >>= prescaler-1) instead of 15-case switch for dividing uint64 by power-of-2 prescaler values - Period: Replace switch statements with compact uint16 lookup tables for both Timer0 and Timer1, casting to uint64 only when needed This addresses review feedback about inefficient switch-based lookups. On AVR, this approach is significantly smaller: - Bit shifts for uint64 division: ~34 bytes vs ~140 bytes - uint16 tables: 22 bytes code + 32/16 bytes data vs ~140 bytes - Total savings: ~190 bytes (68% reduction) Co-Authored-By: Claude Sonnet 4.5 * examples/pwm: add digispark support and smoketest Add digispark.go configuration for PWM example using Timer1 with pins P1 (LED) and P4. Also add digispark PWM example to GNUmakefile smoketests. Co-Authored-By: Claude Sonnet 4.5 --------- Co-authored-by: Claude Sonnet 4.5 --- GNUmakefile | 2 + src/examples/pwm/digispark.go | 12 ++ src/machine/board_digispark.go | 15 +- src/machine/machine_attiny85.go | 354 ++++++++++++++++++++++++++++++++ 4 files changed, 380 insertions(+), 3 deletions(-) create mode 100644 src/examples/pwm/digispark.go diff --git a/GNUmakefile b/GNUmakefile index 99a654ca7f..322c0b7cff 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -896,6 +896,8 @@ endif @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=digispark examples/blinky1 @$(MD5SUM) test.hex + $(TINYGO) build -size short -o test.hex -target=digispark examples/pwm + @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=digispark -gc=leaking examples/blinky1 @$(MD5SUM) test.hex ifneq ($(XTENSA), 0) diff --git a/src/examples/pwm/digispark.go b/src/examples/pwm/digispark.go new file mode 100644 index 0000000000..848d518546 --- /dev/null +++ b/src/examples/pwm/digispark.go @@ -0,0 +1,12 @@ +//go:build digispark + +package main + +import "machine" + +var ( + // Use Timer1 for PWM (recommended for ATtiny85) + pwm = machine.Timer1 + pinA = machine.P1 // PB1, Timer1 channel A (LED pin) + pinB = machine.P4 // PB4, Timer1 channel B +) diff --git a/src/machine/board_digispark.go b/src/machine/board_digispark.go index f380aae85c..d7106a5544 100644 --- a/src/machine/board_digispark.go +++ b/src/machine/board_digispark.go @@ -2,17 +2,26 @@ package machine +// Digispark is a tiny ATtiny85-based board with 6 I/O pins. +// +// PWM is available on the following pins: +// - P0 (PB0): Timer0 channel A +// - P1 (PB1): Timer0 channel B or Timer1 channel A (LED pin) +// - P4 (PB4): Timer1 channel B +// +// Timer1 is recommended for PWM as it provides more flexible frequency control. + // Return the current CPU frequency in hertz. func CPUFrequency() uint32 { return 16000000 } const ( - P0 Pin = PB0 - P1 Pin = PB1 + P0 Pin = PB0 // PWM available (Timer0 OC0A) + P1 Pin = PB1 // PWM available (Timer0 OC0B or Timer1 OC1A) P2 Pin = PB2 P3 Pin = PB3 - P4 Pin = PB4 + P4 Pin = PB4 // PWM available (Timer1 OC1B) P5 Pin = PB5 LED = P1 diff --git a/src/machine/machine_attiny85.go b/src/machine/machine_attiny85.go index 33424c6052..27adaf948c 100644 --- a/src/machine/machine_attiny85.go +++ b/src/machine/machine_attiny85.go @@ -21,3 +21,357 @@ func (p Pin) getPortMask() (*volatile.Register8, uint8) { // Very simple for the attiny85, which only has a single port. return avr.PORTB, 1 << uint8(p) } + +// PWM is one PWM peripheral, which consists of a counter and two output +// channels (that can be connected to two fixed pins). You can set the frequency +// using SetPeriod, but only for all the channels in this PWM peripheral at +// once. +type PWM struct { + num uint8 +} + +var ( + Timer0 = PWM{0} // 8 bit timer for PB0 and PB1 + Timer1 = PWM{1} // 8 bit high-speed timer for PB1 and PB4 +) + +// GTCCR bits for Timer1 that are not defined in the device file +const ( + gtccrPWM1B = 0x40 // Pulse Width Modulator B Enable + gtccrCOM1B0 = 0x10 // Comparator B Output Mode bit 0 + gtccrCOM1B1 = 0x20 // Comparator B Output Mode bit 1 +) + +// Configure enables and configures this PWM. +// +// For Timer0, there is only a limited number of periods available, namely the +// CPU frequency divided by 256 and again divided by 1, 8, 64, 256, or 1024. +// For a MCU running at 8MHz, this would be a period of 32µs, 256µs, 2048µs, +// 8192µs, or 32768µs. +// +// For Timer1, the period is more flexible as it uses OCR1C as the top value. +// Timer1 also supports more prescaler values (1 to 16384). +func (pwm PWM) Configure(config PWMConfig) error { + switch pwm.num { + case 0: // Timer/Counter 0 (8-bit) + // Calculate the timer prescaler. + var prescaler uint8 + switch config.Period { + case 0, (uint64(1e9) * 256 * 1) / uint64(CPUFrequency()): + prescaler = 1 + case (uint64(1e9) * 256 * 8) / uint64(CPUFrequency()): + prescaler = 2 + case (uint64(1e9) * 256 * 64) / uint64(CPUFrequency()): + prescaler = 3 + case (uint64(1e9) * 256 * 256) / uint64(CPUFrequency()): + prescaler = 4 + case (uint64(1e9) * 256 * 1024) / uint64(CPUFrequency()): + prescaler = 5 + default: + return ErrPWMPeriodTooLong + } + + avr.TCCR0B.Set(prescaler) + // Set the PWM mode to fast PWM (mode = 3). + avr.TCCR0A.Set(avr.TCCR0A_WGM00 | avr.TCCR0A_WGM01) + + case 1: // Timer/Counter 1 (8-bit high-speed) + // Timer1 on ATtiny85 is different from ATmega328: + // - It's 8-bit with configurable top (OCR1C) + // - Has more prescaler options (1-16384) + // - PWM mode is enabled per-channel via PWM1A/PWM1B bits + var top uint64 + if config.Period == 0 { + // Use a top appropriate for LEDs. + top = 0xff + } else { + // Calculate top value: top = period * (CPUFrequency / 1e9) + top = config.Period * (uint64(CPUFrequency()) / 1000000) / 1000 + } + + // Timer1 prescaler values: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384 + const maxTop = 256 + var prescaler uint8 + switch { + case top <= maxTop: + prescaler = 1 // prescaler 1 + case top/2 <= maxTop: + prescaler = 2 // prescaler 2 + top /= 2 + case top/4 <= maxTop: + prescaler = 3 // prescaler 4 + top /= 4 + case top/8 <= maxTop: + prescaler = 4 // prescaler 8 + top /= 8 + case top/16 <= maxTop: + prescaler = 5 // prescaler 16 + top /= 16 + case top/32 <= maxTop: + prescaler = 6 // prescaler 32 + top /= 32 + case top/64 <= maxTop: + prescaler = 7 // prescaler 64 + top /= 64 + case top/128 <= maxTop: + prescaler = 8 // prescaler 128 + top /= 128 + case top/256 <= maxTop: + prescaler = 9 // prescaler 256 + top /= 256 + case top/512 <= maxTop: + prescaler = 10 // prescaler 512 + top /= 512 + case top/1024 <= maxTop: + prescaler = 11 // prescaler 1024 + top /= 1024 + case top/2048 <= maxTop: + prescaler = 12 // prescaler 2048 + top /= 2048 + case top/4096 <= maxTop: + prescaler = 13 // prescaler 4096 + top /= 4096 + case top/8192 <= maxTop: + prescaler = 14 // prescaler 8192 + top /= 8192 + case top/16384 <= maxTop: + prescaler = 15 // prescaler 16384 + top /= 16384 + default: + return ErrPWMPeriodTooLong + } + + // Set prescaler (CS1[3:0] bits) + avr.TCCR1.Set(prescaler) + // Set top value + avr.OCR1C.Set(uint8(top - 1)) + } + return nil +} + +// SetPeriod updates the period of this PWM peripheral. +// To set a particular frequency, use the following formula: +// +// period = 1e9 / frequency +// +// If you use a period of 0, a period that works well for LEDs will be picked. +// +// SetPeriod will not change the prescaler, but also won't change the current +// value in any of the channels. This means that you may need to update the +// value for the particular channel. +// +// Note that you cannot pick any arbitrary period after the PWM peripheral has +// been configured. If you want to switch between frequencies, pick the lowest +// frequency (longest period) once when calling Configure and adjust the +// frequency here as needed. +func (pwm PWM) SetPeriod(period uint64) error { + if pwm.num == 0 { + return ErrPWMPeriodTooLong // Timer0 doesn't support dynamic period + } + + // Timer1 can adjust period via OCR1C + var top uint64 + if period == 0 { + top = 0xff + } else { + top = period * (uint64(CPUFrequency()) / 1000000) / 1000 + } + + // Get current prescaler + prescaler := avr.TCCR1.Get() & 0x0f + // Timer1 prescaler values follow a power-of-2 pattern: + // prescaler n maps to divisor 2^(n-1), so we can use a simple shift + if prescaler > 0 && prescaler <= 15 { + top >>= (prescaler - 1) + } + + if top > 256 { + return ErrPWMPeriodTooLong + } + + avr.OCR1C.Set(uint8(top - 1)) + avr.TCNT1.Set(0) + + return nil +} + +// Top returns the current counter top, for use in duty cycle calculation. It +// will only change with a call to Configure or SetPeriod, otherwise it is +// constant. +// +// The value returned here is hardware dependent. In general, it's best to treat +// it as an opaque value that can be divided by some number and passed to Set +// (see Set documentation for more information). +func (pwm PWM) Top() uint32 { + if pwm.num == 1 { + // Timer1 has configurable top via OCR1C + return uint32(avr.OCR1C.Get()) + 1 + } + // Timer0 goes from 0 to 0xff (256 in total) + return 256 +} + +// Counter returns the current counter value of the timer in this PWM +// peripheral. It may be useful for debugging. +func (pwm PWM) Counter() uint32 { + switch pwm.num { + case 0: + return uint32(avr.TCNT0.Get()) + case 1: + return uint32(avr.TCNT1.Get()) + } + return 0 +} + +// Prescaler lookup tables using uint16 (more efficient than uint64 on AVR) +// Timer0 prescaler lookup table (index 0-7 maps to prescaler bits) +var timer0Prescalers = [8]uint16{0, 1, 8, 64, 256, 1024, 0, 0} + +// Timer1 prescaler lookup table (index 0-15 maps to prescaler bits) +var timer1Prescalers = [16]uint16{0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384} + +// Period returns the used PWM period in nanoseconds. It might deviate slightly +// from the configured period due to rounding. +func (pwm PWM) Period() uint64 { + var prescaler uint64 + switch pwm.num { + case 0: + prescalerBits := avr.TCCR0B.Get() & 0x7 + prescaler = uint64(timer0Prescalers[prescalerBits]) + if prescaler == 0 { + return 0 + } + case 1: + prescalerBits := avr.TCCR1.Get() & 0x0f + prescaler = uint64(timer1Prescalers[prescalerBits]) + if prescaler == 0 { + return 0 + } + } + top := uint64(pwm.Top()) + return prescaler * top * 1000 / uint64(CPUFrequency()/1e6) +} + +// Channel returns a PWM channel for the given pin. +func (pwm PWM) Channel(pin Pin) (uint8, error) { + pin.Configure(PinConfig{Mode: PinOutput}) + pin.Low() + switch pwm.num { + case 0: + switch pin { + case PB0: // OC0A + avr.TCCR0A.SetBits(avr.TCCR0A_COM0A1) + return 0, nil + case PB1: // OC0B + avr.TCCR0A.SetBits(avr.TCCR0A_COM0B1) + return 1, nil + } + case 1: + switch pin { + case PB1: // OC1A + // Enable PWM on channel A + avr.TCCR1.SetBits(avr.TCCR1_PWM1A | avr.TCCR1_COM1A1) + return 0, nil + case PB4: // OC1B + // Enable PWM on channel B (controlled via GTCCR) + avr.GTCCR.SetBits(gtccrPWM1B | gtccrCOM1B1) + return 1, nil + } + } + return 0, ErrInvalidOutputPin +} + +// SetInverting sets whether to invert the output of this channel. +// Without inverting, a 25% duty cycle would mean the output is high for 25% of +// the time and low for the rest. Inverting flips the output as if a NOT gate +// was placed at the output, meaning that the output would be 25% low and 75% +// high with a duty cycle of 25%. +func (pwm PWM) SetInverting(channel uint8, inverting bool) { + switch pwm.num { + case 0: + switch channel { + case 0: // channel A, PB0 + if inverting { + avr.PORTB.SetBits(1 << 0) + avr.TCCR0A.SetBits(avr.TCCR0A_COM0A0) + } else { + avr.PORTB.ClearBits(1 << 0) + avr.TCCR0A.ClearBits(avr.TCCR0A_COM0A0) + } + case 1: // channel B, PB1 + if inverting { + avr.PORTB.SetBits(1 << 1) + avr.TCCR0A.SetBits(avr.TCCR0A_COM0B0) + } else { + avr.PORTB.ClearBits(1 << 1) + avr.TCCR0A.ClearBits(avr.TCCR0A_COM0B0) + } + } + case 1: + switch channel { + case 0: // channel A, PB1 + if inverting { + avr.PORTB.SetBits(1 << 1) + avr.TCCR1.SetBits(avr.TCCR1_COM1A0) + } else { + avr.PORTB.ClearBits(1 << 1) + avr.TCCR1.ClearBits(avr.TCCR1_COM1A0) + } + case 1: // channel B, PB4 + if inverting { + avr.PORTB.SetBits(1 << 4) + avr.GTCCR.SetBits(gtccrCOM1B0) + } else { + avr.PORTB.ClearBits(1 << 4) + avr.GTCCR.ClearBits(gtccrCOM1B0) + } + } + } +} + +// Set updates the channel value. This is used to control the channel duty +// cycle, in other words the fraction of time the channel output is high (or low +// when inverted). For example, to set it to a 25% duty cycle, use: +// +// pwm.Set(channel, pwm.Top() / 4) +// +// pwm.Set(channel, 0) will set the output to low and pwm.Set(channel, +// pwm.Top()) will set the output to high, assuming the output isn't inverted. +func (pwm PWM) Set(channel uint8, value uint32) { + switch pwm.num { + case 0: + switch channel { + case 0: // channel A, PB0 + if value == 0 { + avr.TCCR0A.ClearBits(avr.TCCR0A_COM0A1) + } else { + avr.OCR0A.Set(uint8(value - 1)) + avr.TCCR0A.SetBits(avr.TCCR0A_COM0A1) + } + case 1: // channel B, PB1 + if value == 0 { + avr.TCCR0A.ClearBits(avr.TCCR0A_COM0B1) + } else { + avr.OCR0B.Set(uint8(value - 1)) + avr.TCCR0A.SetBits(avr.TCCR0A_COM0B1) + } + } + case 1: + switch channel { + case 0: // channel A, PB1 + if value == 0 { + avr.TCCR1.ClearBits(avr.TCCR1_COM1A1) + } else { + avr.OCR1A.Set(uint8(value - 1)) + avr.TCCR1.SetBits(avr.TCCR1_COM1A1) + } + case 1: // channel B, PB4 + if value == 0 { + avr.GTCCR.ClearBits(gtccrCOM1B1) + } else { + avr.OCR1B.Set(uint8(value - 1)) + avr.GTCCR.SetBits(gtccrCOM1B1) + } + } + } +} From 8bd2233b57f03ec2ef699786fb4f6256b4b3dbae Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 13 Jan 2026 01:22:20 +0100 Subject: [PATCH 02/11] machine/rp: use the blockReset() and unresetBlockWait() helper functions for all peripheral reset/unreset operations Signed-off-by: deadprogram --- src/machine/machine_rp2_adc.go | 6 ++---- src/machine/machine_rp2_i2c.go | 7 ++----- src/machine/machine_rp2_spi.go | 7 ++----- src/machine/machine_rp2_uart.go | 6 ++---- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/machine/machine_rp2_adc.go b/src/machine/machine_rp2_adc.go index e0d6a459a9..12ff152dc9 100644 --- a/src/machine/machine_rp2_adc.go +++ b/src/machine/machine_rp2_adc.go @@ -19,10 +19,8 @@ var adcAref uint32 // InitADC resets the ADC peripheral. func InitADC() { - rp.RESETS.RESET.SetBits(rp.RESETS_RESET_ADC) - rp.RESETS.RESET.ClearBits(rp.RESETS_RESET_ADC) - for !rp.RESETS.RESET_DONE.HasBits(rp.RESETS_RESET_ADC) { - } + resetBlock(rp.RESETS_RESET_ADC) + unresetBlockWait(rp.RESETS_RESET_ADC) // enable ADC rp.ADC.CS.Set(rp.ADC_CS_EN) adcAref = 3300 diff --git a/src/machine/machine_rp2_i2c.go b/src/machine/machine_rp2_i2c.go index 54a5e5357b..50e2e8a277 100644 --- a/src/machine/machine_rp2_i2c.go +++ b/src/machine/machine_rp2_i2c.go @@ -259,10 +259,7 @@ func (i2c *I2C) init(config I2CConfig) error { //go:inline func (i2c *I2C) reset() { resetVal := i2c.deinit() - rp.RESETS.RESET.ClearBits(resetVal) - // Wait until reset is done. - for !rp.RESETS.RESET_DONE.HasBits(resetVal) { - } + unresetBlockWait(resetVal) } // deinit sets reset bit for I2C. Must call reset to reenable I2C after deinit. @@ -276,7 +273,7 @@ func (i2c *I2C) deinit() (resetVal uint32) { resetVal = rp.RESETS_RESET_I2C1 } // Perform I2C reset. - rp.RESETS.RESET.SetBits(resetVal) + resetBlock(resetVal) return resetVal } diff --git a/src/machine/machine_rp2_spi.go b/src/machine/machine_rp2_spi.go index 75e4f86b7b..f3fb256f61 100644 --- a/src/machine/machine_rp2_spi.go +++ b/src/machine/machine_rp2_spi.go @@ -212,10 +212,7 @@ func (spi *SPI) setFormat(mode uint8) { //go:inline func (spi *SPI) reset() { resetVal := spi.deinit() - rp.RESETS.RESET.ClearBits(resetVal) - // Wait until reset is done. - for !rp.RESETS.RESET_DONE.HasBits(resetVal) { - } + unresetBlockWait(resetVal) } //go:inline @@ -227,7 +224,7 @@ func (spi *SPI) deinit() (resetVal uint32) { resetVal = rp.RESETS_RESET_SPI1 } // Perform SPI reset. - rp.RESETS.RESET.SetBits(resetVal) + resetBlock(resetVal) return resetVal } diff --git a/src/machine/machine_rp2_uart.go b/src/machine/machine_rp2_uart.go index 872418a766..5961d3a869 100644 --- a/src/machine/machine_rp2_uart.go +++ b/src/machine/machine_rp2_uart.go @@ -148,10 +148,8 @@ func initUART(uart *UART) { } // reset UART - rp.RESETS.RESET.SetBits(resetVal) - rp.RESETS.RESET.ClearBits(resetVal) - for !rp.RESETS.RESET_DONE.HasBits(resetVal) { - } + resetBlock(resetVal) + unresetBlockWait(resetVal) } // handleInterrupt should be called from the appropriate interrupt handler for From 1fe934e8a8a9779d966d60dd91868f761be8bf91 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 13 Jan 2026 17:33:37 +0100 Subject: [PATCH 03/11] machine/rp: add Close function to UART to allow for removing all system resources/power usage Signed-off-by: deadprogram --- src/machine/machine_rp2_uart.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/machine/machine_rp2_uart.go b/src/machine/machine_rp2_uart.go index 5961d3a869..37e2ca9c2a 100644 --- a/src/machine/machine_rp2_uart.go +++ b/src/machine/machine_rp2_uart.go @@ -73,6 +73,27 @@ func (uart *UART) Configure(config UARTConfig) error { return nil } +// Close the UART and disable its interrupt/power use. +func (uart *UART) Close() error { + uart.Interrupt.Disable() + + // Disable UART. + uart.Bus.UARTCR.ClearBits(rp.UART0_UARTCR_UARTEN) + + var resetVal uint32 + switch { + case uart.Bus == rp.UART0: + resetVal = rp.RESETS_RESET_UART0 + case uart.Bus == rp.UART1: + resetVal = rp.RESETS_RESET_UART1 + } + + // reset UART + resetBlock(resetVal) + + return nil +} + // SetBaudRate sets the baudrate to be used for the UART. func (uart *UART) SetBaudRate(br uint32) { div := 8 * CPUFrequency() / br From 1876b65b18f9f9e5f3a4fe9f407b2a9d7562a7b4 Mon Sep 17 00:00:00 2001 From: Nia Waldvogel Date: Sun, 28 Dec 2025 14:59:13 -0500 Subject: [PATCH 04/11] compiler: simplify createObjectLayout This simplifies the process of constructing and encoding layout bitmaps. Instead of creating big integers and merging them, we can create a pre-sized bitmap and set positions within it. This also changes the encoding logic to allow larger layouts to be encoded inline. We would previously not encode a layout inline unless the size was less than the width of the data field. This is overly conservative. A layout can be encoded inline as long as: 1. The size fits within the size field. 2. All set bits in the bitmap fit into the data field. --- compiler/gc.go | 3 + compiler/llvm.go | 182 ++++++++++++++++++++-------------------- compiler/testdata/gc.go | 8 ++ compiler/testdata/gc.ll | 12 ++- 4 files changed, 110 insertions(+), 95 deletions(-) diff --git a/compiler/gc.go b/compiler/gc.go index fc0e6e687f..5ca79b91ba 100644 --- a/compiler/gc.go +++ b/compiler/gc.go @@ -99,6 +99,9 @@ func typeHasPointers(t llvm.Type) bool { } return false case llvm.ArrayTypeKind: + if t.ArrayLength() == 0 { + return false + } if typeHasPointers(t.ElementType()) { return true } diff --git a/compiler/llvm.go b/compiler/llvm.go index de387b39c0..7ce6c7d615 100644 --- a/compiler/llvm.go +++ b/compiler/llvm.go @@ -1,10 +1,10 @@ package compiler import ( + "encoding/binary" "fmt" "go/token" "go/types" - "math/big" "strings" "github.com/tinygo-org/tinygo/compileopts" @@ -231,6 +231,12 @@ func (c *compilerContext) makeGlobalArray(buf []byte, name string, elementType l // // For details on what's in this value, see src/runtime/gc_precise.go. func (c *compilerContext) createObjectLayout(t llvm.Type, pos token.Pos) llvm.Value { + if !typeHasPointers(t) { + // There are no pointers in this type, so we can simplify the layout. + layout := (uint64(1) << 1) | 1 + return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.dataPtrType) + } + // Use the element type for arrays. This works even for nested arrays. for { kind := t.TypeKind() @@ -248,54 +254,29 @@ func (c *compilerContext) createObjectLayout(t llvm.Type, pos token.Pos) llvm.Va break } - // Do a few checks to see whether we need to generate any object layout - // information at all. + // Create the pointer bitmap. objectSizeBytes := c.targetData.TypeAllocSize(t) - pointerSize := c.targetData.TypeAllocSize(c.dataPtrType) - pointerAlignment := c.targetData.PrefTypeAlignment(c.dataPtrType) - if objectSizeBytes < pointerSize { - // Too small to contain a pointer. - layout := (uint64(1) << 1) | 1 - return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.dataPtrType) - } - bitmap := c.getPointerBitmap(t, pos) - if bitmap.BitLen() == 0 { - // There are no pointers in this type, so we can simplify the layout. - // TODO: this can be done in many other cases, e.g. when allocating an - // array (like [4][]byte, which repeats a slice 4 times). - layout := (uint64(1) << 1) | 1 - return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.dataPtrType) - } - if objectSizeBytes%uint64(pointerAlignment) != 0 { - // This shouldn't happen except for packed structs, which aren't - // currently used. - c.addError(pos, "internal error: unexpected object size for object with pointer field") - return llvm.ConstNull(c.dataPtrType) - } - objectSizeWords := objectSizeBytes / uint64(pointerAlignment) + pointerAlignment := uint64(c.targetData.PrefTypeAlignment(c.dataPtrType)) + bitmapLen := objectSizeBytes / pointerAlignment + bitmapBytes := (bitmapLen + 7) / 8 + bitmap := make([]byte, bitmapBytes, max(bitmapBytes, 8)) + c.buildPointerBitmap(bitmap, pointerAlignment, pos, t, 0) + // Try to encode the layout inline. + pointerSize := c.targetData.TypeAllocSize(c.dataPtrType) pointerBits := pointerSize * 8 - var sizeFieldBits uint64 - switch pointerBits { - case 16: - sizeFieldBits = 4 - case 32: - sizeFieldBits = 5 - case 64: - sizeFieldBits = 6 - default: - panic("unknown pointer size") - } - layoutFieldBits := pointerBits - 1 - sizeFieldBits - - // Try to emit the value as an inline integer. This is possible in most - // cases. - if objectSizeWords < layoutFieldBits { - // If it can be stored directly in the pointer value, do so. - // The runtime knows that if the least significant bit of the pointer is - // set, the pointer contains the value itself. - layout := bitmap.Uint64()<<(sizeFieldBits+1) | (objectSizeWords << 1) | 1 - return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.dataPtrType) + if bitmapLen < pointerBits { + rawMask := binary.LittleEndian.Uint64(bitmap[0:8]) + layout := rawMask*pointerBits + bitmapLen + layout <<= 1 + layout |= 1 + + // Check if the layout fits. + layout &= 1<>1)/pointerBits == rawMask { + // No set bits were shifted off. + return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.dataPtrType) + } } // Unfortunately, the object layout is too big to fit in a pointer-sized @@ -303,25 +284,24 @@ func (c *compilerContext) createObjectLayout(t llvm.Type, pos token.Pos) llvm.Va // Try first whether the global already exists. All objects with a // particular name have the same type, so this is possible. - globalName := "runtime/gc.layout:" + fmt.Sprintf("%d-%0*x", objectSizeWords, (objectSizeWords+15)/16, bitmap) + globalName := "runtime/gc.layout:" + fmt.Sprintf("%d-%0*x", bitmapLen, (bitmapLen+15)/16, bitmap) global := c.mod.NamedGlobal(globalName) if !global.IsNil() { return global } // Create the global initializer. - bitmapBytes := make([]byte, int(objectSizeWords+7)/8) - bitmap.FillBytes(bitmapBytes) - reverseBytes(bitmapBytes) // big-endian to little-endian - var bitmapByteValues []llvm.Value - for _, b := range bitmapBytes { - bitmapByteValues = append(bitmapByteValues, llvm.ConstInt(c.ctx.Int8Type(), uint64(b), false)) + bitmapByteValues := make([]llvm.Value, bitmapBytes) + i8 := c.ctx.Int8Type() + for i, b := range bitmap { + bitmapByteValues[i] = llvm.ConstInt(i8, uint64(b), false) } initializer := c.ctx.ConstStruct([]llvm.Value{ - llvm.ConstInt(c.uintptrType, objectSizeWords, false), - llvm.ConstArray(c.ctx.Int8Type(), bitmapByteValues), + llvm.ConstInt(c.uintptrType, bitmapLen, false), + llvm.ConstArray(i8, bitmapByteValues), }, false) + // Create the actual global. global = llvm.AddGlobal(c.mod, initializer.Type(), globalName) global.SetInitializer(initializer) global.SetUnnamedAddr(true) @@ -329,6 +309,7 @@ func (c *compilerContext) createObjectLayout(t llvm.Type, pos token.Pos) llvm.Va global.SetLinkage(llvm.LinkOnceODRLinkage) if c.targetData.PrefTypeAlignment(c.uintptrType) < 2 { // AVR doesn't have alignment by default. + // The lowest bit must be unset to distinguish this from an inline layout. global.SetAlignment(2) } if c.Debug && pos != token.NoPos { @@ -360,52 +341,71 @@ func (c *compilerContext) createObjectLayout(t llvm.Type, pos token.Pos) llvm.Va return global } -// getPointerBitmap scans the given LLVM type for pointers and sets bits in a -// bigint at the word offset that contains a pointer. This scan is recursive. -func (c *compilerContext) getPointerBitmap(typ llvm.Type, pos token.Pos) *big.Int { - alignment := c.targetData.PrefTypeAlignment(c.dataPtrType) - switch typ.TypeKind() { +// buildPointerBitmap scans the given LLVM type for pointers and sets bits in a +// bitmap at the word offset that contains a pointer. This scan is recursive. +func (c *compilerContext) buildPointerBitmap( + dst []byte, + ptrAlign uint64, + pos token.Pos, + t llvm.Type, + offset uint64, +) { + switch t.TypeKind() { case llvm.IntegerTypeKind, llvm.FloatTypeKind, llvm.DoubleTypeKind: - return big.NewInt(0) + // These types do not contain pointers. + case llvm.PointerTypeKind: - return big.NewInt(1) + // Set the corresponding position in the bitmap. + dst[offset/8] |= 1 << (offset % 8) + case llvm.StructTypeKind: - ptrs := big.NewInt(0) - for i, subtyp := range typ.StructElementTypes() { - subptrs := c.getPointerBitmap(subtyp, pos) - if subptrs.BitLen() == 0 { - continue - } - offset := c.targetData.ElementOffset(typ, i) - if offset%uint64(alignment) != 0 { - // This error will let the compilation fail, but by continuing - // the error can still easily be shown. - c.addError(pos, "internal error: allocated struct contains unaligned pointer") + // Recurse over struct elements. + for i, et := range t.StructElementTypes() { + eo := c.targetData.ElementOffset(t, i) + if eo%uint64(ptrAlign) != 0 { + if typeHasPointers(et) { + // This error will let the compilation fail, but by continuing + // the error can still easily be shown. + c.addError(pos, "internal error: allocated struct contains unaligned pointer") + } continue } - subptrs.Lsh(subptrs, uint(offset)/uint(alignment)) - ptrs.Or(ptrs, subptrs) + c.buildPointerBitmap( + dst, + ptrAlign, + pos, + et, + offset+(eo/ptrAlign), + ) } - return ptrs + case llvm.ArrayTypeKind: - subtyp := typ.ElementType() - subptrs := c.getPointerBitmap(subtyp, pos) - ptrs := big.NewInt(0) - if subptrs.BitLen() == 0 { - return ptrs + // Recurse over array elements. + len := t.ArrayLength() + if len <= 0 { + return } - elementSize := c.targetData.TypeAllocSize(subtyp) - if elementSize%uint64(alignment) != 0 { - // This error will let the compilation fail (but continues so that - // other errors can be shown). - c.addError(pos, "internal error: allocated array contains unaligned pointer") - return ptrs + et := t.ElementType() + elementSize := c.targetData.TypeAllocSize(et) + if elementSize%ptrAlign != 0 { + if typeHasPointers(et) { + // This error will let the compilation fail (but continues so that + // other errors can be shown). + c.addError(pos, "internal error: allocated array contains unaligned pointer") + } + return } - for i := 0; i < typ.ArrayLength(); i++ { - ptrs.Lsh(ptrs, uint(elementSize)/uint(alignment)) - ptrs.Or(ptrs, subptrs) + elementSize /= ptrAlign + for i := 0; i < len; i++ { + c.buildPointerBitmap( + dst, + ptrAlign, + pos, + et, + offset+uint64(i)*elementSize, + ) } - return ptrs + default: // Should not happen. panic("unknown LLVM type") diff --git a/compiler/testdata/gc.go b/compiler/testdata/gc.go index 20e5967028..9aa00a4c6f 100644 --- a/compiler/testdata/gc.go +++ b/compiler/testdata/gc.go @@ -24,6 +24,10 @@ var ( x *byte y [61]uintptr } + struct5 *struct { + x *byte + y [30]uintptr + } slice1 []byte slice2 []*int @@ -58,6 +62,10 @@ func newStruct() { x *byte y [61]uintptr }) + struct5 = new(struct { + x *byte + y [30]uintptr + }) } func newFuncValue() *func() { diff --git a/compiler/testdata/gc.ll b/compiler/testdata/gc.ll index d2be74cbcf..42a278b66e 100644 --- a/compiler/testdata/gc.ll +++ b/compiler/testdata/gc.ll @@ -16,11 +16,12 @@ target triple = "wasm32-unknown-wasi" @main.struct2 = hidden global ptr null, align 4 @main.struct3 = hidden global ptr null, align 4 @main.struct4 = hidden global ptr null, align 4 +@main.struct5 = hidden global ptr null, align 4 @main.slice1 = hidden global { ptr, i32, i32 } zeroinitializer, align 4 @main.slice2 = hidden global { ptr, i32, i32 } zeroinitializer, align 4 @main.slice3 = hidden global { ptr, i32, i32 } zeroinitializer, align 4 -@"runtime/gc.layout:62-2000000000000001" = linkonce_odr unnamed_addr constant { i32, [8 x i8] } { i32 62, [8 x i8] c"\01\00\00\00\00\00\00 " } -@"runtime/gc.layout:62-0001" = linkonce_odr unnamed_addr constant { i32, [8 x i8] } { i32 62, [8 x i8] c"\01\00\00\00\00\00\00\00" } +@"runtime/gc.layout:62-0100000000000020" = linkonce_odr unnamed_addr constant { i32, [8 x i8] } { i32 62, [8 x i8] c"\01\00\00\00\00\00\00 " } +@"runtime/gc.layout:62-0100000000000000" = linkonce_odr unnamed_addr constant { i32, [8 x i8] } { i32 62, [8 x i8] c"\01\00\00\00\00\00\00\00" } @"reflect/types.type:basic:complex128" = linkonce_odr constant { i8, ptr } { i8 80, ptr @"reflect/types.type:pointer:basic:complex128" }, align 4 @"reflect/types.type:pointer:basic:complex128" = linkonce_odr constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:basic:complex128" }, align 4 @@ -80,12 +81,15 @@ entry: %new1 = call align 4 dereferenceable(8) ptr @runtime.alloc(i32 8, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #3 call void @runtime.trackPointer(ptr nonnull %new1, ptr nonnull %stackalloc, ptr undef) #3 store ptr %new1, ptr @main.struct2, align 4 - %new2 = call align 4 dereferenceable(248) ptr @runtime.alloc(i32 248, ptr nonnull @"runtime/gc.layout:62-2000000000000001", ptr undef) #3 + %new2 = call align 4 dereferenceable(248) ptr @runtime.alloc(i32 248, ptr nonnull @"runtime/gc.layout:62-0100000000000020", ptr undef) #3 call void @runtime.trackPointer(ptr nonnull %new2, ptr nonnull %stackalloc, ptr undef) #3 store ptr %new2, ptr @main.struct3, align 4 - %new3 = call align 4 dereferenceable(248) ptr @runtime.alloc(i32 248, ptr nonnull @"runtime/gc.layout:62-0001", ptr undef) #3 + %new3 = call align 4 dereferenceable(248) ptr @runtime.alloc(i32 248, ptr nonnull @"runtime/gc.layout:62-0100000000000000", ptr undef) #3 call void @runtime.trackPointer(ptr nonnull %new3, ptr nonnull %stackalloc, ptr undef) #3 store ptr %new3, ptr @main.struct4, align 4 + %new4 = call align 4 dereferenceable(124) ptr @runtime.alloc(i32 124, ptr nonnull inttoptr (i32 127 to ptr), ptr undef) #3 + call void @runtime.trackPointer(ptr nonnull %new4, ptr nonnull %stackalloc, ptr undef) #3 + store ptr %new4, ptr @main.struct5, align 4 ret void } From 707d37a4c1bbc279c7e3859cfddb6435b7bb4282 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 14 Jan 2026 09:55:22 +0100 Subject: [PATCH 05/11] chore: update version to 0.41.0-dev Signed-off-by: deadprogram --- goenv/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/goenv/version.go b/goenv/version.go index 423f95906e..9ade0e0b79 100644 --- a/goenv/version.go +++ b/goenv/version.go @@ -10,7 +10,7 @@ import ( // Version of TinyGo. // Update this value before release of new version of software. -const version = "0.40.1" +const version = "0.41.0-dev" // Return TinyGo version, either in the form 0.30.0 or as a development version // (like 0.30.0-dev-abcd012). From a0069b6282164081f044eda45143e98c4371419d Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Sat, 17 Jan 2026 08:59:13 -0800 Subject: [PATCH 06/11] testdata: more corpus entries (#5182) * testdata: more corpus entries * testdata: remove skipwasi for dchest/siphash build issues --- testdata/corpus.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/testdata/corpus.yaml b/testdata/corpus.yaml index 0ed29adbe4..36ac337cf6 100644 --- a/testdata/corpus.yaml +++ b/testdata/corpus.yaml @@ -29,8 +29,7 @@ - repo: github.com/dgryski/go-camellia - repo: github.com/dgryski/go-change - repo: github.com/dgryski/go-chaskey - tags: appengine noasm - skipwasi: true # siphash has build tag issues + tags: appengine noasm # for dchest/siphash - repo: github.com/dgryski/go-clefia - repo: github.com/dgryski/go-clockpro - repo: github.com/dgryski/go-cobs @@ -56,7 +55,6 @@ - repo: github.com/dgryski/go-linlog - repo: github.com/dgryski/go-maglev tags: appengine # for dchest/siphash - skipwasi: true - repo: github.com/dgryski/go-marvin32 - repo: github.com/dgryski/go-md5crypt - repo: github.com/dgryski/go-metro @@ -66,7 +64,6 @@ tags: noasm - repo: github.com/dgryski/go-mpchash tags: appengine # for dchest/siphash - skipwasi: true - repo: github.com/dgryski/go-neeva - repo: github.com/dgryski/go-nibz - repo: github.com/dgryski/go-nibblesort @@ -289,3 +286,8 @@ - repo: github.com/philhofer/fwd - repo: github.com/blevesearch/sear - repo: github.com/steveyen/gtreap +- repo: github.com/orsinium-labs/tinymath +- repo: github.com/orsinium-labs/jsony +- repo: github.com/tidwall/gjson +- repo: github.com/dchest/siphash + tags: appengine From 5d8e071bfb195872263631f66474a7cf7dfb2a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Sat, 17 Jan 2026 21:22:15 +0100 Subject: [PATCH 07/11] machine/attiny85: add USI-based SPI support (#5181) * machine/attiny85: add USI-based SPI support Implement SPI communication for ATTiny85 using the USI (Universal Serial Interface) hardware in three-wire mode. The ATTiny85 lacks dedicated SPI hardware but can emulate SPI using the USI module with software clock strobing. Implementation details: - Configure USI in three-wire mode for SPI operation - Use clock strobing technique to shift data in/out - Pin mapping: PB2 (SCK), PB1 (MOSI/DO), PB0 (MISO/DI) - Support both Transfer() and Tx() methods The implementation uses the USI control register (USICR) to toggle the clock pin, which triggers automatic bit shifting in hardware. This is more efficient than pure software bit-banging. Current limitations: - Frequency configuration not yet implemented (runs at max software speed) - Only SPI Mode 0 (CPOL=0, CPHA=0) supported - Only MSB-first bit order supported Co-Authored-By: Claude Sonnet 4.5 Co-authored-by: Ona * machine/attiny85: add SPI frequency configuration support Add software-based frequency control for USI SPI. The ATtiny85 USI lacks hardware prescalers, so frequency is controlled via delay loops between clock toggles. - Calculate delay cycles based on requested frequency and CPU clock - Fast path (no delay) when frequency is 0 or max speed requested - Delay loop uses nop instructions for timing control Co-authored-by: Ona * machine/attiny85: add SPI mode configuration support Add support for all 4 SPI modes (Mode 0-3) using USI hardware: - Mode 0 (CPOL=0, CPHA=0): Clock idle low, sample on rising edge - Mode 1 (CPOL=0, CPHA=1): Clock idle low, sample on falling edge - Mode 2 (CPOL=1, CPHA=0): Clock idle high, sample on falling edge - Mode 3 (CPOL=1, CPHA=1): Clock idle high, sample on rising edge CPOL is controlled by setting the clock pin idle state. CPHA is controlled via the USICS0 bit in USICR. Co-authored-by: Ona * machine/attiny85: add LSB-first bit order support Add software-based LSB-first support for USI SPI. The USI hardware only supports MSB-first, so bit reversal is done in software before sending and after receiving. Uses an efficient parallel bit swap algorithm (3 operations) to reverse the byte. Co-authored-by: Ona * GNUmakefile: add mcp3008 SPI example to digispark smoketest Test the USI-based SPI implementation for ATtiny85/digispark. Co-authored-by: Ona * machine/attiny85: minimize SPI RAM footprint Reduce SPI struct from ~14 bytes to 1 byte to fit in ATtiny85's limited 512 bytes of RAM. Changes: - Remove register pointers (use avr.USIDR/USISR/USICR directly) - Remove pin fields (USI pins are fixed: PB0/PB1/PB2) - Remove CS pin management (user must handle CS) - Remove frequency control (runs at max speed) - Remove LSBFirst support The SPI struct now only stores the USICR configuration byte. Co-authored-by: Ona * Revert "machine/attiny85: minimize SPI RAM footprint" This reverts commit 387ccad494a5f0419c1e274d228eb100284f28fd. Co-authored-by: Ona * machine/attiny85: reduce SPI RAM usage by 10 bytes Remove unnecessary fields from SPI struct while keeping all functionality: - Remove register pointers (use avr.USIDR/USISR/USICR directly) - Remove pin fields (USI pins are fixed: PB0/PB1/PB2) - Remove CS pin (user must manage it, standard practice) Kept functional fields: - delayCycles for frequency control - usicrValue for SPI mode support - lsbFirst for bit order support SPI struct reduced from 14 bytes to 4 bytes. Co-authored-by: Ona --------- Co-authored-by: Ona --- GNUmakefile | 2 + src/machine/machine_attiny85.go | 167 ++++++++++++++++++++++++++++++++ src/machine/spi.go | 2 +- src/machine/spi_tx.go | 2 +- 4 files changed, 171 insertions(+), 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 322c0b7cff..fc8d71d998 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -898,6 +898,8 @@ endif @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=digispark examples/pwm @$(MD5SUM) test.hex + $(TINYGO) build -size short -o test.hex -target=digispark examples/mcp3008 + @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=digispark -gc=leaking examples/blinky1 @$(MD5SUM) test.hex ifneq ($(XTENSA), 0) diff --git a/src/machine/machine_attiny85.go b/src/machine/machine_attiny85.go index 27adaf948c..6d31846b5a 100644 --- a/src/machine/machine_attiny85.go +++ b/src/machine/machine_attiny85.go @@ -375,3 +375,170 @@ func (pwm PWM) Set(channel uint8, value uint32) { } } } + +// SPIConfig is used to store config info for SPI. +type SPIConfig struct { + Frequency uint32 + LSBFirst bool + Mode uint8 +} + +// SPI is the USI-based SPI implementation for ATTiny85. +// The ATTiny85 doesn't have dedicated SPI hardware, but uses the USI +// (Universal Serial Interface) in three-wire mode. +// +// Fixed pin mapping (directly controlled by USI hardware): +// - PB2: SCK (clock) +// - PB1: DO/MOSI (data out) +// - PB0: DI/MISO (data in) +// +// Note: CS pin must be managed by the user. +type SPI struct { + // Delay cycles for frequency control (0 = max speed) + delayCycles uint16 + + // USICR value configured for the selected SPI mode + usicrValue uint8 + + // LSB-first mode (requires software bit reversal) + lsbFirst bool +} + +// SPI0 is the USI-based SPI interface on the ATTiny85 +var SPI0 = SPI{} + +// Configure sets up the USI for SPI communication. +// Note: The user must configure and control the CS pin separately. +func (s *SPI) Configure(config SPIConfig) error { + // Configure USI pins (fixed by hardware) + // PB1 (DO/MOSI) -> OUTPUT + // PB2 (USCK/SCK) -> OUTPUT + // PB0 (DI/MISO) -> INPUT + PB1.Configure(PinConfig{Mode: PinOutput}) + PB2.Configure(PinConfig{Mode: PinOutput}) + PB0.Configure(PinConfig{Mode: PinInput}) + + // Reset USI registers + avr.USIDR.Set(0) + avr.USISR.Set(0) + + // Configure USI for SPI mode: + // - USIWM0: Three-wire mode (SPI) + // - USICS1: External clock source (software controlled via USITC) + // - USICLK: Clock strobe - enables counter increment on USITC toggle + // - USICS0: Controls clock phase (CPHA) + // + // SPI Modes: + // Mode 0 (CPOL=0, CPHA=0): Clock idle low, sample on rising edge + // Mode 1 (CPOL=0, CPHA=1): Clock idle low, sample on falling edge + // Mode 2 (CPOL=1, CPHA=0): Clock idle high, sample on falling edge + // Mode 3 (CPOL=1, CPHA=1): Clock idle high, sample on rising edge + // + // For USI, USICS0 controls the sampling edge when USICS1=1: + // USICS0=0: Positive edge (rising) + // USICS0=1: Negative edge (falling) + switch config.Mode { + case Mode0: // CPOL=0, CPHA=0: idle low, sample rising + PB2.Low() + s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK + case Mode1: // CPOL=0, CPHA=1: idle low, sample falling + PB2.Low() + s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICS0 | avr.USICR_USICLK + case Mode2: // CPOL=1, CPHA=0: idle high, sample falling + PB2.High() + s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICS0 | avr.USICR_USICLK + case Mode3: // CPOL=1, CPHA=1: idle high, sample rising + PB2.High() + s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK + default: // Default to Mode 0 + PB2.Low() + s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK + } + avr.USICR.Set(s.usicrValue) + + // Calculate delay cycles for frequency control + // Each bit transfer requires 2 clock toggles (rising + falling edge) + // The loop overhead is approximately 10-15 cycles per toggle on AVR + // We calculate additional delay cycles needed to achieve the target frequency + if config.Frequency > 0 && config.Frequency < CPUFrequency()/2 { + // Cycles per half-period = CPUFrequency / (2 * Frequency) + // Subtract loop overhead (~15 cycles) to get delay cycles + cyclesPerHalfPeriod := CPUFrequency() / (2 * config.Frequency) + const loopOverhead = 15 + if cyclesPerHalfPeriod > loopOverhead { + s.delayCycles = uint16(cyclesPerHalfPeriod - loopOverhead) + } else { + s.delayCycles = 0 + } + } else { + // Max speed - no delay + s.delayCycles = 0 + } + + // Store LSBFirst setting for use in Transfer + s.lsbFirst = config.LSBFirst + + return nil +} + +// reverseByte reverses the bit order of a byte (MSB <-> LSB) +// Used for LSB-first SPI mode since USI hardware only supports MSB-first +func reverseByte(b byte) byte { + b = (b&0xF0)>>4 | (b&0x0F)<<4 + b = (b&0xCC)>>2 | (b&0x33)<<2 + b = (b&0xAA)>>1 | (b&0x55)<<1 + return b +} + +// Transfer performs a single byte SPI transfer (send and receive simultaneously) +// This implements the USI-based SPI transfer using the "clock strobing" technique +func (s *SPI) Transfer(b byte) (byte, error) { + // For LSB-first mode, reverse the bits before sending + // USI hardware only supports MSB-first, so we do it in software + if s.lsbFirst { + b = reverseByte(b) + } + + // Load the byte to transmit into the USI Data Register + avr.USIDR.Set(b) + + // Clear the counter overflow flag by writing 1 to it (AVR quirk) + // This also resets the 4-bit counter to 0 + avr.USISR.Set(avr.USISR_USIOIF) + + // Clock the data out/in + // We need 16 clock toggles (8 bits × 2 edges per bit) + // The USI counter counts each clock edge, so it overflows at 16 + // After 16 toggles, the clock returns to its idle state (set by CPOL in Configure) + // + // IMPORTANT: Only toggle USITC here! + // - USITC toggles the clock pin + // - The USICR mode bits (USIWM0, USICS1, USICS0, USICLK) were set in Configure() + // - SetBits preserves those bits and only sets USITC + if s.delayCycles == 0 { + // Fast path: no delay, run at maximum speed + for !avr.USISR.HasBits(avr.USISR_USIOIF) { + avr.USICR.SetBits(avr.USICR_USITC) + } + } else { + // Frequency-controlled path: add delay between clock toggles + for !avr.USISR.HasBits(avr.USISR_USIOIF) { + avr.USICR.SetBits(avr.USICR_USITC) + // Delay loop for frequency control + // Each iteration is approximately 3 cycles on AVR (dec, brne) + for i := s.delayCycles; i > 0; i-- { + avr.Asm("nop") + } + } + } + + // Get the received byte + result := avr.USIDR.Get() + + // For LSB-first mode, reverse the received bits + if s.lsbFirst { + result = reverseByte(result) + } + + return result, nil +} diff --git a/src/machine/spi.go b/src/machine/spi.go index 9a1033ca7d..fa507b961d 100644 --- a/src/machine/spi.go +++ b/src/machine/spi.go @@ -1,4 +1,4 @@ -//go:build !baremetal || atmega || esp32 || fe310 || k210 || nrf || (nxp && !mk66f18) || rp2040 || rp2350 || sam || (stm32 && !stm32f7x2 && !stm32l5x2) +//go:build !baremetal || atmega || attiny85 || esp32 || fe310 || k210 || nrf || (nxp && !mk66f18) || rp2040 || rp2350 || sam || (stm32 && !stm32f7x2 && !stm32l5x2) package machine diff --git a/src/machine/spi_tx.go b/src/machine/spi_tx.go index 97385bb596..aec3f52fe1 100644 --- a/src/machine/spi_tx.go +++ b/src/machine/spi_tx.go @@ -1,4 +1,4 @@ -//go:build atmega || fe310 || k210 || (nxp && !mk66f18) || (stm32 && !stm32f7x2 && !stm32l5x2) +//go:build atmega || attiny85 || fe310 || k210 || (nxp && !mk66f18) || (stm32 && !stm32f7x2 && !stm32l5x2) // This file implements the SPI Tx function for targets that don't have a custom // (faster) implementation for it. From 7b86f959e22292e55f47371c55345690a2bdad18 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Fri, 16 Jan 2026 13:26:48 -0800 Subject: [PATCH 08/11] compiler: include the scope in the type code name Fixes #5180 --- compiler/interface.go | 28 ++++++++++++++++++++++++++-- testdata/interface.go | 38 ++++++++++++++++++++++++++++++++++++++ testdata/interface.txt | 3 +++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/compiler/interface.go b/compiler/interface.go index e63666834f..30893bcfb6 100644 --- a/compiler/interface.go +++ b/compiler/interface.go @@ -511,14 +511,38 @@ var basicTypeNames = [...]string{ types.UnsafePointer: "unsafe.Pointer", } +// return an integer representing this scope in a package. +func scopeID(pkg *types.Scope, scope *types.Scope) string { + var ids []int + for scope != pkg { + parent := scope.Parent() + for i := range parent.NumChildren() { + if parent.Child(i) == scope { + ids = append(ids, i) + scope = scope.Parent() + break + } + } + } + + var buf []byte + for _, v := range ids { + buf = strconv.AppendInt(buf, int64(v), 10) + buf = append(buf, ':') + } + + return string(buf) +} + // getTypeCodeName returns a name for this type that can be used in the // interface lowering pass to assign type codes as expected by the reflect // package. See getTypeCodeNum. func getTypeCodeName(t types.Type) (string, bool) { switch t := types.Unalias(t).(type) { case *types.Named: - if t.Obj().Parent() != t.Obj().Pkg().Scope() { - return "named:" + t.String() + "$local", true + parent, pkg := t.Obj().Parent(), t.Obj().Pkg().Scope() + if parent != pkg { + return fmt.Sprintf("named:%s$local:%s", t.String(), scopeID(pkg, parent)), true } return "named:" + t.String(), false case *types.Array: diff --git a/testdata/interface.go b/testdata/interface.go index 1a3a838288..674ca04f2c 100644 --- a/testdata/interface.go +++ b/testdata/interface.go @@ -119,6 +119,10 @@ func main() { // check that type asserts to interfaces with no methods work emptyintfcrash() + + // These are part of a test that checks that `main.Foo` can refer to 2+ different entities without getting them confused. + namedFoo() + namedFoo2Nested() } func printItf(val interface{}) { @@ -343,3 +347,37 @@ func emptyintfcrash() { println("x is", x.(int)) } } + +func namedFoo() { + type Foo struct { + A int + } + f1 := &Foo{} + fcopy := copyOf(f1) + f2 := fcopy.(*Foo) + println(f2.A) +} + +func namedFoo2Nested() { + type Foo struct { + A *int + } + f1 := &Foo{} + fcopy := copyOf(f1) + f2 := fcopy.(*Foo) + println(f2.A == nil) + + if f2.A == nil { + type Foo struct { + A *byte + } + nestedf1 := &Foo{} + fcopy := copyOf(nestedf1) + nestedf2 := fcopy.(*Foo) + println(nestedf2.A == nil) + } +} + +func copyOf(src interface{}) (dst interface{}) { + return src +} diff --git a/testdata/interface.txt b/testdata/interface.txt index 55f7ae1def..ced833d1fe 100644 --- a/testdata/interface.txt +++ b/testdata/interface.txt @@ -28,3 +28,6 @@ type is int type is *int type is **int x is 5 +0 +true +true From 332e2b99a3d281ffbe4d2d3bef4f63a2af12d26b Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Mon, 19 Jan 2026 16:59:45 -0800 Subject: [PATCH 09/11] compiler: add scopeID cache --- compiler/interface.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/compiler/interface.go b/compiler/interface.go index 30893bcfb6..4088651c63 100644 --- a/compiler/interface.go +++ b/compiler/interface.go @@ -12,6 +12,7 @@ import ( "go/types" "strconv" "strings" + "sync" "golang.org/x/tools/go/ssa" "tinygo.org/x/go-llvm" @@ -511,8 +512,25 @@ var basicTypeNames = [...]string{ types.UnsafePointer: "unsafe.Pointer", } +var scopeIDCache = struct { + sync.Mutex + scopeid map[*types.Scope]string +}{ + sync.Mutex{}, + make(map[*types.Scope]string), +} + // return an integer representing this scope in a package. func scopeID(pkg *types.Scope, scope *types.Scope) string { + scopeIDCache.Lock() + defer scopeIDCache.Unlock() + + if id := scopeIDCache.scopeid[scope]; id != "" { + return id + } + + entry := scope + var ids []int for scope != pkg { parent := scope.Parent() @@ -531,7 +549,9 @@ func scopeID(pkg *types.Scope, scope *types.Scope) string { buf = append(buf, ':') } - return string(buf) + id := string(buf) + scopeIDCache.scopeid[entry] = id + return id } // getTypeCodeName returns a name for this type that can be used in the From d91670ca9a10d2397bdec97f2fbf5b98ab57af57 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Tue, 20 Jan 2026 07:43:12 -0800 Subject: [PATCH 10/11] compiler/testdata: add .ll test for scope id changes --- compiler/testdata/interface.go | 34 ++++++ compiler/testdata/interface.ll | 200 +++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) diff --git a/compiler/testdata/interface.go b/compiler/testdata/interface.go index 2833ff7717..a39f94fada 100644 --- a/compiler/testdata/interface.go +++ b/compiler/testdata/interface.go @@ -61,3 +61,37 @@ func callFooMethod(itf fooInterface) uint8 { func callErrorMethod(itf error) string { return itf.Error() } + +func namedFoo() { + type Foo struct { + A int + } + f1 := &Foo{} + fcopy := copyOf(f1) + f2 := fcopy.(*Foo) + println(f2.A) +} + +func namedFoo2Nested() { + type Foo struct { + A *int + } + f1 := &Foo{} + fcopy := copyOf(f1) + f2 := fcopy.(*Foo) + println(f2.A == nil) + + if f2.A == nil { + type Foo struct { + A *byte + } + nestedf1 := &Foo{} + fcopy := copyOf(nestedf1) + nestedf2 := fcopy.(*Foo) + println(nestedf2.A == nil) + } +} + +func copyOf(src interface{}) (dst interface{}) { + return src +} diff --git a/compiler/testdata/interface.ll b/compiler/testdata/interface.ll index fcde0158d4..b5fff47482 100644 --- a/compiler/testdata/interface.ll +++ b/compiler/testdata/interface.ll @@ -3,6 +3,7 @@ source_filename = "interface.go" target datalayout = "e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-i128:128-n32:64-S128-ni:1:10:20" target triple = "wasm32-unknown-wasi" +%runtime.structField = type { ptr, ptr } %runtime._interface = type { ptr, ptr } %runtime._string = type { ptr, i32 } @@ -16,6 +17,27 @@ target triple = "wasm32-unknown-wasi" @"reflect/types.type:pointer:interface:{String:func:{}{basic:string}}" = linkonce_odr constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:interface:{String:func:{}{basic:string}}" }, align 4 @"reflect/types.type:interface:{String:func:{}{basic:string}}" = linkonce_odr constant { i8, ptr } { i8 84, ptr @"reflect/types.type:pointer:interface:{String:func:{}{basic:string}}" }, align 4 @"reflect/types.typeid:basic:int" = external constant i8 +@"reflect/types.type:pointer:named:main.Foo$local:11:0:" = internal constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:named:main.Foo$local:11:0:" }, align 4 +@"reflect/types.type:named:main.Foo$local:11:0:" = internal constant { i8, i16, ptr, ptr, ptr, [9 x i8] } { i8 -6, i16 0, ptr @"reflect/types.type:pointer:named:main.Foo$local:11:0:", ptr @"reflect/types.type:struct:{A:basic:int}", ptr @"reflect/types.type.pkgpath:main", [9 x i8] c"main.Foo\00" }, align 4 +@"reflect/types.type.pkgpath:main" = linkonce_odr unnamed_addr constant [5 x i8] c"main\00", align 1 +@"reflect/types.type:struct:{A:basic:int}" = linkonce_odr constant { i8, i16, ptr, ptr, i32, i16, [1 x %runtime.structField] } { i8 -38, i16 0, ptr @"reflect/types.type:pointer:struct:{A:basic:int}", ptr @"reflect/types.type.pkgpath:main", i32 4, i16 1, [1 x %runtime.structField] [%runtime.structField { ptr @"reflect/types.type:basic:int", ptr @"reflect/types.type:struct:{A:basic:int}.A" }] }, align 4 +@"reflect/types.type:pointer:struct:{A:basic:int}" = linkonce_odr constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:struct:{A:basic:int}" }, align 4 +@"reflect/types.type:struct:{A:basic:int}.A" = internal unnamed_addr constant [4 x i8] c"\04\00A\00", align 1 +@"reflect/types.typeid:pointer:named:main.Foo$local:11:0:" = external constant i8 +@"reflect/types.type:pointer:named:main.Foo$local:12:0:" = internal constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:named:main.Foo$local:12:0:" }, align 4 +@"reflect/types.type:named:main.Foo$local:12:0:" = internal constant { i8, i16, ptr, ptr, ptr, [9 x i8] } { i8 -6, i16 0, ptr @"reflect/types.type:pointer:named:main.Foo$local:12:0:", ptr @"reflect/types.type:struct:{A:pointer:basic:int}", ptr @"reflect/types.type.pkgpath:main", [9 x i8] c"main.Foo\00" }, align 4 +@"reflect/types.type:struct:{A:pointer:basic:int}" = linkonce_odr constant { i8, i16, ptr, ptr, i32, i16, [1 x %runtime.structField] } { i8 -38, i16 0, ptr @"reflect/types.type:pointer:struct:{A:pointer:basic:int}", ptr @"reflect/types.type.pkgpath:main", i32 4, i16 1, [1 x %runtime.structField] [%runtime.structField { ptr @"reflect/types.type:pointer:basic:int", ptr @"reflect/types.type:struct:{A:pointer:basic:int}.A" }] }, align 4 +@"reflect/types.type:pointer:struct:{A:pointer:basic:int}" = linkonce_odr constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:struct:{A:pointer:basic:int}" }, align 4 +@"reflect/types.type:struct:{A:pointer:basic:int}.A" = internal unnamed_addr constant [4 x i8] c"\04\00A\00", align 1 +@"reflect/types.typeid:pointer:named:main.Foo$local:12:0:" = external constant i8 +@"reflect/types.type:pointer:named:main.Foo$local:0:0:12:0:" = internal constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:named:main.Foo$local:0:0:12:0:" }, align 4 +@"reflect/types.type:named:main.Foo$local:0:0:12:0:" = internal constant { i8, i16, ptr, ptr, ptr, [9 x i8] } { i8 -6, i16 0, ptr @"reflect/types.type:pointer:named:main.Foo$local:0:0:12:0:", ptr @"reflect/types.type:struct:{A:pointer:basic:uint8}", ptr @"reflect/types.type.pkgpath:main", [9 x i8] c"main.Foo\00" }, align 4 +@"reflect/types.type:struct:{A:pointer:basic:uint8}" = linkonce_odr constant { i8, i16, ptr, ptr, i32, i16, [1 x %runtime.structField] } { i8 -38, i16 0, ptr @"reflect/types.type:pointer:struct:{A:pointer:basic:uint8}", ptr @"reflect/types.type.pkgpath:main", i32 4, i16 1, [1 x %runtime.structField] [%runtime.structField { ptr @"reflect/types.type:pointer:basic:uint8", ptr @"reflect/types.type:struct:{A:pointer:basic:uint8}.A" }] }, align 4 +@"reflect/types.type:pointer:struct:{A:pointer:basic:uint8}" = linkonce_odr constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:struct:{A:pointer:basic:uint8}" }, align 4 +@"reflect/types.type:struct:{A:pointer:basic:uint8}.A" = internal unnamed_addr constant [4 x i8] c"\04\00A\00", align 1 +@"reflect/types.type:pointer:basic:uint8" = linkonce_odr constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:basic:uint8" }, align 4 +@"reflect/types.type:basic:uint8" = linkonce_odr constant { i8, ptr } { i8 -56, ptr @"reflect/types.type:pointer:basic:uint8" }, align 4 +@"reflect/types.typeid:pointer:named:main.Foo$local:0:0:12:0:" = external constant i8 ; Function Attrs: allockind("alloc,zeroed") allocsize(0) declare noalias nonnull ptr @runtime.alloc(i32, ptr, ptr) #0 @@ -130,6 +152,184 @@ entry: declare %runtime._string @"interface:{Error:func:{}{basic:string}}.Error$invoke"(ptr, ptr, ptr) #6 +; Function Attrs: nounwind +define hidden void @main.namedFoo(ptr %context) unnamed_addr #2 { +entry: + %stackalloc = alloca i8, align 1 + %complit = call align 4 dereferenceable(4) ptr @runtime.alloc(i32 4, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #7 + call void @runtime.trackPointer(ptr nonnull %complit, ptr nonnull %stackalloc, ptr undef) #7 + call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:named:main.Foo$local:11:0:", ptr nonnull %stackalloc, ptr undef) #7 + call void @runtime.trackPointer(ptr nonnull %complit, ptr nonnull %stackalloc, ptr undef) #7 + %0 = call %runtime._interface @main.copyOf(ptr nonnull @"reflect/types.type:pointer:named:main.Foo$local:11:0:", ptr nonnull %complit, ptr undef) + %1 = extractvalue %runtime._interface %0, 0 + call void @runtime.trackPointer(ptr %1, ptr nonnull %stackalloc, ptr undef) #7 + %2 = extractvalue %runtime._interface %0, 1 + call void @runtime.trackPointer(ptr %2, ptr nonnull %stackalloc, ptr undef) #7 + %interface.type = extractvalue %runtime._interface %0, 0 + %typecode = call i1 @runtime.typeAssert(ptr %interface.type, ptr nonnull @"reflect/types.typeid:pointer:named:main.Foo$local:11:0:", ptr undef) #7 + br i1 %typecode, label %typeassert.ok, label %typeassert.next + +typeassert.next: ; preds = %typeassert.ok, %entry + %typeassert.value = phi ptr [ null, %entry ], [ %typeassert.value.ptr, %typeassert.ok ] + call void @runtime.interfaceTypeAssert(i1 %typecode, ptr undef) #7 + %3 = icmp eq ptr %typeassert.value, null + br i1 %3, label %gep.throw, label %gep.next + +gep.next: ; preds = %typeassert.next + br i1 false, label %deref.throw, label %deref.next + +deref.next: ; preds = %gep.next + %4 = load i32, ptr %typeassert.value, align 4 + call void @runtime.printlock(ptr undef) #7 + call void @runtime.printint32(i32 %4, ptr undef) #7 + call void @runtime.printnl(ptr undef) #7 + call void @runtime.printunlock(ptr undef) #7 + ret void + +typeassert.ok: ; preds = %entry + %typeassert.value.ptr = extractvalue %runtime._interface %0, 1 + br label %typeassert.next + +gep.throw: ; preds = %typeassert.next + call void @runtime.nilPanic(ptr undef) #7 + unreachable + +deref.throw: ; preds = %gep.next + unreachable +} + +; Function Attrs: nounwind +define hidden %runtime._interface @main.copyOf(ptr %src.typecode, ptr %src.value, ptr %context) unnamed_addr #2 { +entry: + %0 = insertvalue %runtime._interface zeroinitializer, ptr %src.typecode, 0 + %1 = insertvalue %runtime._interface %0, ptr %src.value, 1 + ret %runtime._interface %1 +} + +declare void @runtime.interfaceTypeAssert(i1, ptr) #1 + +declare void @runtime.nilPanic(ptr) #1 + +declare void @runtime.printlock(ptr) #1 + +declare void @runtime.printint32(i32, ptr) #1 + +declare void @runtime.printnl(ptr) #1 + +declare void @runtime.printunlock(ptr) #1 + +; Function Attrs: nounwind +define hidden void @main.namedFoo2Nested(ptr %context) unnamed_addr #2 { +entry: + %stackalloc = alloca i8, align 1 + %complit = call align 4 dereferenceable(4) ptr @runtime.alloc(i32 4, ptr nonnull inttoptr (i32 67 to ptr), ptr undef) #7 + call void @runtime.trackPointer(ptr nonnull %complit, ptr nonnull %stackalloc, ptr undef) #7 + call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:named:main.Foo$local:12:0:", ptr nonnull %stackalloc, ptr undef) #7 + call void @runtime.trackPointer(ptr nonnull %complit, ptr nonnull %stackalloc, ptr undef) #7 + %0 = call %runtime._interface @main.copyOf(ptr nonnull @"reflect/types.type:pointer:named:main.Foo$local:12:0:", ptr nonnull %complit, ptr undef) + %1 = extractvalue %runtime._interface %0, 0 + call void @runtime.trackPointer(ptr %1, ptr nonnull %stackalloc, ptr undef) #7 + %2 = extractvalue %runtime._interface %0, 1 + call void @runtime.trackPointer(ptr %2, ptr nonnull %stackalloc, ptr undef) #7 + %interface.type = extractvalue %runtime._interface %0, 0 + %typecode = call i1 @runtime.typeAssert(ptr %interface.type, ptr nonnull @"reflect/types.typeid:pointer:named:main.Foo$local:12:0:", ptr undef) #7 + br i1 %typecode, label %typeassert.ok, label %typeassert.next + +typeassert.next: ; preds = %typeassert.ok, %entry + %typeassert.value = phi ptr [ null, %entry ], [ %typeassert.value.ptr, %typeassert.ok ] + call void @runtime.interfaceTypeAssert(i1 %typecode, ptr undef) #7 + %3 = icmp eq ptr %typeassert.value, null + br i1 %3, label %gep.throw, label %gep.next + +gep.next: ; preds = %typeassert.next + br i1 false, label %deref.throw, label %deref.next + +deref.next: ; preds = %gep.next + %4 = load ptr, ptr %typeassert.value, align 4 + call void @runtime.trackPointer(ptr %4, ptr nonnull %stackalloc, ptr undef) #7 + %5 = icmp eq ptr %4, null + call void @runtime.printlock(ptr undef) #7 + call void @runtime.printbool(i1 %5, ptr undef) #7 + call void @runtime.printnl(ptr undef) #7 + call void @runtime.printunlock(ptr undef) #7 + br i1 false, label %gep.throw1, label %gep.next2 + +gep.next2: ; preds = %deref.next + br i1 false, label %deref.throw3, label %deref.next4 + +deref.next4: ; preds = %gep.next2 + %6 = load ptr, ptr %typeassert.value, align 4 + call void @runtime.trackPointer(ptr %6, ptr nonnull %stackalloc, ptr undef) #7 + %7 = icmp eq ptr %6, null + br i1 %7, label %if.then, label %if.done + +typeassert.ok: ; preds = %entry + %typeassert.value.ptr = extractvalue %runtime._interface %0, 1 + br label %typeassert.next + +if.then: ; preds = %deref.next4 + %complit5 = call align 4 dereferenceable(4) ptr @runtime.alloc(i32 4, ptr nonnull inttoptr (i32 67 to ptr), ptr undef) #7 + call void @runtime.trackPointer(ptr nonnull %complit5, ptr nonnull %stackalloc, ptr undef) #7 + call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:named:main.Foo$local:0:0:12:0:", ptr nonnull %stackalloc, ptr undef) #7 + call void @runtime.trackPointer(ptr nonnull %complit5, ptr nonnull %stackalloc, ptr undef) #7 + %8 = call %runtime._interface @main.copyOf(ptr nonnull @"reflect/types.type:pointer:named:main.Foo$local:0:0:12:0:", ptr nonnull %complit5, ptr undef) + %9 = extractvalue %runtime._interface %8, 0 + call void @runtime.trackPointer(ptr %9, ptr nonnull %stackalloc, ptr undef) #7 + %10 = extractvalue %runtime._interface %8, 1 + call void @runtime.trackPointer(ptr %10, ptr nonnull %stackalloc, ptr undef) #7 + %interface.type6 = extractvalue %runtime._interface %8, 0 + %typecode7 = call i1 @runtime.typeAssert(ptr %interface.type6, ptr nonnull @"reflect/types.typeid:pointer:named:main.Foo$local:0:0:12:0:", ptr undef) #7 + br i1 %typecode7, label %typeassert.ok8, label %typeassert.next9 + +typeassert.next9: ; preds = %typeassert.ok8, %if.then + %typeassert.value11 = phi ptr [ null, %if.then ], [ %typeassert.value.ptr10, %typeassert.ok8 ] + call void @runtime.interfaceTypeAssert(i1 %typecode7, ptr undef) #7 + %11 = icmp eq ptr %typeassert.value11, null + br i1 %11, label %gep.throw12, label %gep.next13 + +gep.next13: ; preds = %typeassert.next9 + br i1 false, label %deref.throw14, label %deref.next15 + +deref.next15: ; preds = %gep.next13 + %12 = load ptr, ptr %typeassert.value11, align 4 + call void @runtime.trackPointer(ptr %12, ptr nonnull %stackalloc, ptr undef) #7 + %13 = icmp eq ptr %12, null + call void @runtime.printlock(ptr undef) #7 + call void @runtime.printbool(i1 %13, ptr undef) #7 + call void @runtime.printnl(ptr undef) #7 + call void @runtime.printunlock(ptr undef) #7 + br label %if.done + +typeassert.ok8: ; preds = %if.then + %typeassert.value.ptr10 = extractvalue %runtime._interface %8, 1 + br label %typeassert.next9 + +if.done: ; preds = %deref.next15, %deref.next4 + ret void + +gep.throw: ; preds = %typeassert.next + call void @runtime.nilPanic(ptr undef) #7 + unreachable + +deref.throw: ; preds = %gep.next + unreachable + +gep.throw1: ; preds = %deref.next + unreachable + +deref.throw3: ; preds = %gep.next2 + unreachable + +gep.throw12: ; preds = %typeassert.next9 + call void @runtime.nilPanic(ptr undef) #7 + unreachable + +deref.throw14: ; preds = %gep.next13 + unreachable +} + +declare void @runtime.printbool(i1, ptr) #1 + attributes #0 = { allockind("alloc,zeroed") allocsize(0) "alloc-family"="runtime.alloc" "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" } attributes #1 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" } attributes #2 = { nounwind "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" } From e56e3e5890b585d68bb876bb166903a977680b90 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Tue, 20 Jan 2026 09:57:23 -0800 Subject: [PATCH 11/11] compiler: move scope id cache to compilerContext --- compiler/compiler.go | 2 ++ compiler/interface.go | 74 +++++++++++++++++++------------------------ 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/compiler/compiler.go b/compiler/compiler.go index 8707e1bc93..7826c00e1f 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -92,6 +92,7 @@ type compilerContext struct { pkg *types.Package packageDir string // directory for this package runtimePkg *types.Package + scopeIdx map[*types.Scope]int } // newCompilerContext returns a new compiler context ready for use, most @@ -106,6 +107,7 @@ func newCompilerContext(moduleName string, machine llvm.TargetMachine, config *C targetData: machine.CreateTargetData(), functionInfos: map[*ssa.Function]functionInfo{}, astComments: map[string]*ast.CommentGroup{}, + scopeIdx: map[*types.Scope]int{}, } c.ctx = llvm.NewContext() diff --git a/compiler/interface.go b/compiler/interface.go index 4088651c63..f2d48ebd72 100644 --- a/compiler/interface.go +++ b/compiler/interface.go @@ -12,7 +12,6 @@ import ( "go/types" "strconv" "strings" - "sync" "golang.org/x/tools/go/ssa" "tinygo.org/x/go-llvm" @@ -159,7 +158,7 @@ func (c *compilerContext) getTypeCode(typ types.Type) llvm.Value { } } - typeCodeName, isLocal := getTypeCodeName(typ) + typeCodeName, isLocal := c.getTypeCodeName(typ) globalName := "reflect/types.type:" + typeCodeName var global llvm.Value if isLocal { @@ -512,35 +511,27 @@ var basicTypeNames = [...]string{ types.UnsafePointer: "unsafe.Pointer", } -var scopeIDCache = struct { - sync.Mutex - scopeid map[*types.Scope]string -}{ - sync.Mutex{}, - make(map[*types.Scope]string), -} - // return an integer representing this scope in a package. -func scopeID(pkg *types.Scope, scope *types.Scope) string { - scopeIDCache.Lock() - defer scopeIDCache.Unlock() - - if id := scopeIDCache.scopeid[scope]; id != "" { - return id - } - - entry := scope - +func (c *compilerContext) getScopeID(pkg *types.Scope, scope *types.Scope) string { var ids []int + for scope != pkg { - parent := scope.Parent() - for i := range parent.NumChildren() { - if parent.Child(i) == scope { - ids = append(ids, i) - scope = scope.Parent() - break + if idx, ok := c.scopeIdx[scope]; ok { + ids = append(ids, idx) + } else { + // flesh out scope idx for all children of this parent + parent := scope.Parent() + for i := range parent.NumChildren() { + child := parent.Child(i) + if child == scope { + c.scopeIdx[scope] = i + break + } } + ids = append(ids, c.scopeIdx[scope]) } + // move up a level + scope = scope.Parent() } var buf []byte @@ -550,28 +541,27 @@ func scopeID(pkg *types.Scope, scope *types.Scope) string { } id := string(buf) - scopeIDCache.scopeid[entry] = id return id } // getTypeCodeName returns a name for this type that can be used in the // interface lowering pass to assign type codes as expected by the reflect // package. See getTypeCodeNum. -func getTypeCodeName(t types.Type) (string, bool) { +func (c *compilerContext) getTypeCodeName(t types.Type) (string, bool) { switch t := types.Unalias(t).(type) { case *types.Named: parent, pkg := t.Obj().Parent(), t.Obj().Pkg().Scope() if parent != pkg { - return fmt.Sprintf("named:%s$local:%s", t.String(), scopeID(pkg, parent)), true + return fmt.Sprintf("named:%s$local:%s", t.String(), c.getScopeID(pkg, parent)), true } return "named:" + t.String(), false case *types.Array: - s, isLocal := getTypeCodeName(t.Elem()) + s, isLocal := c.getTypeCodeName(t.Elem()) return "array:" + strconv.FormatInt(t.Len(), 10) + ":" + s, isLocal case *types.Basic: return "basic:" + basicTypeNames[t.Kind()], false case *types.Chan: - s, isLocal := getTypeCodeName(t.Elem()) + s, isLocal := c.getTypeCodeName(t.Elem()) var dir string switch t.Dir() { case types.SendOnly: @@ -591,7 +581,7 @@ func getTypeCodeName(t types.Type) (string, bool) { if !token.IsExported(name) { name = t.Method(i).Pkg().Path() + "." + name } - s, local := getTypeCodeName(t.Method(i).Type()) + s, local := c.getTypeCodeName(t.Method(i).Type()) if local { isLocal = true } @@ -599,17 +589,17 @@ func getTypeCodeName(t types.Type) (string, bool) { } return "interface:" + "{" + strings.Join(methods, ",") + "}", isLocal case *types.Map: - keyType, keyLocal := getTypeCodeName(t.Key()) - elemType, elemLocal := getTypeCodeName(t.Elem()) + keyType, keyLocal := c.getTypeCodeName(t.Key()) + elemType, elemLocal := c.getTypeCodeName(t.Elem()) return "map:" + "{" + keyType + "," + elemType + "}", keyLocal || elemLocal case *types.Pointer: - s, isLocal := getTypeCodeName(t.Elem()) + s, isLocal := c.getTypeCodeName(t.Elem()) return "pointer:" + s, isLocal case *types.Signature: isLocal := false params := make([]string, t.Params().Len()) for i := 0; i < t.Params().Len(); i++ { - s, local := getTypeCodeName(t.Params().At(i).Type()) + s, local := c.getTypeCodeName(t.Params().At(i).Type()) if local { isLocal = true } @@ -617,7 +607,7 @@ func getTypeCodeName(t types.Type) (string, bool) { } results := make([]string, t.Results().Len()) for i := 0; i < t.Results().Len(); i++ { - s, local := getTypeCodeName(t.Results().At(i).Type()) + s, local := c.getTypeCodeName(t.Results().At(i).Type()) if local { isLocal = true } @@ -625,7 +615,7 @@ func getTypeCodeName(t types.Type) (string, bool) { } return "func:" + "{" + strings.Join(params, ",") + "}{" + strings.Join(results, ",") + "}", isLocal case *types.Slice: - s, isLocal := getTypeCodeName(t.Elem()) + s, isLocal := c.getTypeCodeName(t.Elem()) return "slice:" + s, isLocal case *types.Struct: elems := make([]string, t.NumFields()) @@ -635,7 +625,7 @@ func getTypeCodeName(t types.Type) (string, bool) { if t.Field(i).Embedded() { embedded = "#" } - s, local := getTypeCodeName(t.Field(i).Type()) + s, local := c.getTypeCodeName(t.Field(i).Type()) if local { isLocal = true } @@ -753,7 +743,7 @@ func (b *builder) createTypeAssert(expr *ssa.TypeAssert) llvm.Value { commaOk = b.CreateCall(fn.GlobalValueType(), fn, []llvm.Value{actualTypeNum}, "") } } else { - name, _ := getTypeCodeName(expr.AssertedType) + name, _ := b.getTypeCodeName(expr.AssertedType) globalName := "reflect/types.typeid:" + name assertedTypeCodeGlobal := b.mod.NamedGlobal(globalName) if assertedTypeCodeGlobal.IsNil() { @@ -830,7 +820,7 @@ func (c *compilerContext) getMethodsString(itf *types.Interface) string { // getInterfaceImplementsFunc returns a declared function that works as a type // switch. The interface lowering pass will define this function. func (c *compilerContext) getInterfaceImplementsFunc(assertedType types.Type) llvm.Value { - s, _ := getTypeCodeName(assertedType.Underlying()) + s, _ := c.getTypeCodeName(assertedType.Underlying()) fnName := s + ".$typeassert" llvmFn := c.mod.NamedFunction(fnName) if llvmFn.IsNil() { @@ -847,7 +837,7 @@ func (c *compilerContext) getInterfaceImplementsFunc(assertedType types.Type) ll // thunk is declared, not defined: it will be defined by the interface lowering // pass. func (c *compilerContext) getInvokeFunction(instr *ssa.CallCommon) llvm.Value { - s, _ := getTypeCodeName(instr.Value.Type().Underlying()) + s, _ := c.getTypeCodeName(instr.Value.Type().Underlying()) fnName := s + "." + instr.Method.Name() + "$invoke" llvmFn := c.mod.NamedFunction(fnName) if llvmFn.IsNil() {