From 7ef06d5c4ca4e3ba549a32588af87b4f286cf90b Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Thu, 8 Jan 2026 14:38:45 -0800 Subject: [PATCH 1/4] builder: claim we support 1.26 now --- builder/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/config.go b/builder/config.go index bef366f8c8..736fa08ec2 100644 --- a/builder/config.go +++ b/builder/config.go @@ -26,7 +26,7 @@ func NewConfig(options *compileopts.Options) (*compileopts.Config, error) { // Version range supported by TinyGo. const minorMin = 19 - const minorMax = 25 + const minorMax = 26 // Check that we support this Go toolchain version. gorootMajor, gorootMinor, err := goenv.GetGorootVersion() From 49fb6b60874a85a9195a96b1d9a45e9b791d2b60 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Thu, 8 Jan 2026 14:39:03 -0800 Subject: [PATCH 2/4] src/internal/itoa: resurrect from Go stdlib We use this a bunch. Upstream switched to `internal/strconv` which is much heavier. --- loader/goroot.go | 1 + src/internal/itoa/itoa.go | 57 ++++++++++++++++++++++++++++++++++ src/internal/itoa/itoa_test.go | 51 ++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 src/internal/itoa/itoa.go create mode 100644 src/internal/itoa/itoa_test.go diff --git a/loader/goroot.go b/loader/goroot.go index 00a7124d80..6a0f002413 100644 --- a/loader/goroot.go +++ b/loader/goroot.go @@ -245,6 +245,7 @@ func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool { "internal/cm/": false, "internal/futex/": false, "internal/fuzz/": false, + "internal/itoa": false, "internal/reflectlite/": false, "internal/gclayout": false, "internal/task/": false, diff --git a/src/internal/itoa/itoa.go b/src/internal/itoa/itoa.go new file mode 100644 index 0000000000..4340ae0e2d --- /dev/null +++ b/src/internal/itoa/itoa.go @@ -0,0 +1,57 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Simple conversions to avoid depending on strconv. + +package itoa + +// Itoa converts val to a decimal string. +func Itoa(val int) string { + if val < 0 { + return "-" + Uitoa(uint(-val)) + } + return Uitoa(uint(val)) +} + +// Uitoa converts val to a decimal string. +func Uitoa(val uint) string { + if val == 0 { // avoid string allocation + return "0" + } + var buf [20]byte // big enough for 64bit value base 10 + i := len(buf) - 1 + for val >= 10 { + q := val / 10 + buf[i] = byte('0' + val - q*10) + i-- + val = q + } + // val < 10 + buf[i] = byte('0' + val) + return string(buf[i:]) +} + +const hex = "0123456789abcdef" + +// Uitox converts val (a uint) to a hexadecimal string. +func Uitox(val uint) string { + if val == 0 { // avoid string allocation + return "0x0" + } + var buf [20]byte // big enough for 64bit value base 16 + 0x + i := len(buf) - 1 + for val >= 16 { + q := val / 16 + buf[i] = hex[val%16] + i-- + val = q + } + // val < 16 + buf[i] = hex[val%16] + i-- + buf[i] = 'x' + i-- + buf[i] = '0' + return string(buf[i:]) +} diff --git a/src/internal/itoa/itoa_test.go b/src/internal/itoa/itoa_test.go new file mode 100644 index 0000000000..8bed888532 --- /dev/null +++ b/src/internal/itoa/itoa_test.go @@ -0,0 +1,51 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package itoa_test + +import ( + "fmt" + "internal/itoa" + "math" + "testing" +) + +var ( + minInt64 int64 = math.MinInt64 + maxInt64 int64 = math.MaxInt64 + maxUint64 uint64 = math.MaxUint64 +) + +func TestItoa(t *testing.T) { + tests := []int{int(minInt64), math.MinInt32, -999, -100, -1, 0, 1, 100, 999, math.MaxInt32, int(maxInt64)} + for _, tt := range tests { + got := itoa.Itoa(tt) + want := fmt.Sprint(tt) + if want != got { + t.Fatalf("Itoa(%d) = %s, want %s", tt, got, want) + } + } +} + +func TestUitoa(t *testing.T) { + tests := []uint{0, 1, 100, 999, math.MaxUint32, uint(maxUint64)} + for _, tt := range tests { + got := itoa.Uitoa(tt) + want := fmt.Sprint(tt) + if want != got { + t.Fatalf("Uitoa(%d) = %s, want %s", tt, got, want) + } + } +} + +func TestUitox(t *testing.T) { + tests := []uint{0, 1, 15, 100, 999, math.MaxUint32, uint(maxUint64)} + for _, tt := range tests { + got := itoa.Uitox(tt) + want := fmt.Sprintf("%#x", tt) + if want != got { + t.Fatalf("Uitox(%x) = %s, want %s", tt, got, want) + } + } +} From 2579355db81132b4c365e7dd8ec621eccf116d86 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Wed, 18 Mar 2026 11:00:09 -0700 Subject: [PATCH 3/4] runtime: split syscall functions for darwin changes for 1.26 --- src/runtime/os_darwin.go | 67 -------------------------------- src/runtime/os_darwin_go125.go | 70 ++++++++++++++++++++++++++++++++++ src/runtime/os_darwin_go126.go | 69 +++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 67 deletions(-) create mode 100644 src/runtime/os_darwin_go125.go create mode 100644 src/runtime/os_darwin_go126.go diff --git a/src/runtime/os_darwin.go b/src/runtime/os_darwin.go index df5c598807..7197c43973 100644 --- a/src/runtime/os_darwin.go +++ b/src/runtime/os_darwin.go @@ -130,73 +130,6 @@ func syscall_Getpagesize() int { return int(libc_getpagesize()) } -// Call "system calls" (actually: libc functions) in a special way. -// - Most calls calls return a C int (which is 32-bits), and -1 on failure. -// - syscallX* is for functions that return a 64-bit integer (and also return -// -1 on failure). -// - syscallPtr is for functions that return a pointer on success or NULL on -// failure. -// - rawSyscall seems to avoid some stack modifications, which isn't relevant -// to TinyGo. - -//go:linkname syscall_syscall syscall.syscall -func syscall_syscall(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) { - // For TinyGo we don't need to do anything special to call C functions. - return syscall_rawSyscall(fn, a1, a2, a3) -} - -//go:linkname syscall_rawSyscall syscall.rawSyscall -func syscall_rawSyscall(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) { - result := call_syscall(fn, a1, a2, a3) - r1 = uintptr(result) - if result == -1 { - // Syscall returns -1 on failure. - err = uintptr(*libc_errno_location()) - } - return -} - -//go:linkname syscall_syscallX syscall.syscallX -func syscall_syscallX(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) { - r1 = call_syscallX(fn, a1, a2, a3) - if int64(r1) == -1 { - // Syscall returns -1 on failure. - err = uintptr(*libc_errno_location()) - } - return -} - -//go:linkname syscall_syscallPtr syscall.syscallPtr -func syscall_syscallPtr(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) { - r1 = call_syscallX(fn, a1, a2, a3) - if r1 == 0 { - // Syscall returns a pointer on success, or NULL on failure. - err = uintptr(*libc_errno_location()) - } - return -} - -//go:linkname syscall_syscall6 syscall.syscall6 -func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr) { - result := call_syscall6(fn, a1, a2, a3, a4, a5, a6) - r1 = uintptr(result) - if result == -1 { - // Syscall returns -1 on failure. - err = uintptr(*libc_errno_location()) - } - return -} - -//go:linkname syscall_syscall6X syscall.syscall6X -func syscall_syscall6X(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr) { - r1 = call_syscall6X(fn, a1, a2, a3, a4, a5, a6) - if int64(r1) == -1 { - // Syscall returns -1 on failure. - err = uintptr(*libc_errno_location()) - } - return -} - // uint32_t arc4random(void); // //export arc4random diff --git a/src/runtime/os_darwin_go125.go b/src/runtime/os_darwin_go125.go new file mode 100644 index 0000000000..e48bbbe09a --- /dev/null +++ b/src/runtime/os_darwin_go125.go @@ -0,0 +1,70 @@ +//go:build darwin && !go1.26 + +package runtime + +// Call "system calls" (actually: libc functions) in a special way. +// - Most calls calls return a C int (which is 32-bits), and -1 on failure. +// - syscallX* is for functions that return a 64-bit integer (and also return +// -1 on failure). +// - syscallPtr is for functions that return a pointer on success or NULL on +// failure. +// - rawSyscall seems to avoid some stack modifications, which isn't relevant +// to TinyGo. + +//go:linkname syscall_syscall syscall.syscall +func syscall_syscall(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) { + // For TinyGo we don't need to do anything special to call C functions. + return syscall_rawSyscall(fn, a1, a2, a3) +} + +//go:linkname syscall_rawSyscall syscall.rawSyscall +func syscall_rawSyscall(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) { + result := call_syscall(fn, a1, a2, a3) + r1 = uintptr(result) + if result == -1 { + // Syscall returns -1 on failure. + err = uintptr(*libc_errno_location()) + } + return +} + +//go:linkname syscall_syscallX syscall.syscallX +func syscall_syscallX(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) { + r1 = call_syscallX(fn, a1, a2, a3) + if int64(r1) == -1 { + // Syscall returns -1 on failure. + err = uintptr(*libc_errno_location()) + } + return +} + +//go:linkname syscall_syscallPtr syscall.syscallPtr +func syscall_syscallPtr(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) { + r1 = call_syscallX(fn, a1, a2, a3) + if r1 == 0 { + // Syscall returns a pointer on success, or NULL on failure. + err = uintptr(*libc_errno_location()) + } + return +} + +//go:linkname syscall_syscall6 syscall.syscall6 +func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr) { + result := call_syscall6(fn, a1, a2, a3, a4, a5, a6) + r1 = uintptr(result) + if result == -1 { + // Syscall returns -1 on failure. + err = uintptr(*libc_errno_location()) + } + return +} + +//go:linkname syscall_syscall6X syscall.syscall6X +func syscall_syscall6X(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr) { + r1 = call_syscall6X(fn, a1, a2, a3, a4, a5, a6) + if int64(r1) == -1 { + // Syscall returns -1 on failure. + err = uintptr(*libc_errno_location()) + } + return +} diff --git a/src/runtime/os_darwin_go126.go b/src/runtime/os_darwin_go126.go new file mode 100644 index 0000000000..1845fa513b --- /dev/null +++ b/src/runtime/os_darwin_go126.go @@ -0,0 +1,69 @@ +//go:build darwin && go1.26 + +package runtime + +// syscall_syscalln is a wrapper around the libc call with variable arguments. +// +//go:nosplit +//go:linkname syscall_syscalln syscall.syscalln +func syscall_syscalln(fn uintptr, args ...uintptr) (r1, r2, err uintptr) { + r1, r2, err = syscall_rawsyscalln(fn, args...) + return r1, r2, err +} + +// syscall_rawsyscalln is a wrapper around the libc call with variable arguments. +// +//go:linkname syscall_rawsyscalln syscall.rawsyscalln +//go:nosplit +func syscall_rawsyscalln(fn uintptr, args ...uintptr) (r1, r2, err uintptr) { + + var a1, a2, a3, a4, a5, a6 uintptr + + switch len(args) { + case 3: + a3 = args[2] + fallthrough + case 2: + a2 = args[1] + fallthrough + case 1: + a1 = args[0] + fallthrough + case 0: + return runtime_syscall(fn, a1, a2, a3) + + case 6: + a6 = args[5] + fallthrough + case 5: + a5 = args[4] + fallthrough + case 4: + a3 = args[3] + + a1, a2, a3 = args[0], args[1], args[2] + return runtime_syscall6(fn, a1, a2, a3, a4, a5, a6) + } + + panic("syscall args not handled") +} + +func runtime_syscall(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) { + result := call_syscall(fn, a1, a2, a3) + r1 = uintptr(result) + if result == -1 { + // Syscall returns -1 on failure. + err = uintptr(*libc_errno_location()) + } + return +} + +func runtime_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr) { + result := call_syscall6(fn, a1, a2, a3, a4, a5, a6) + r1 = uintptr(result) + if result == -1 { + // Syscall returns -1 on failure. + err = uintptr(*libc_errno_location()) + } + return +} From bff93b37e8c9f3b7412c3754c79b35a30b389c9e Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Wed, 18 Mar 2026 11:27:00 -0700 Subject: [PATCH 4/4] internal/abi: add EscapeNonString, EscapeToResultNonString --- src/internal/abi/escape.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/internal/abi/escape.go b/src/internal/abi/escape.go index 1f4c763312..5dcdd636b7 100644 --- a/src/internal/abi/escape.go +++ b/src/internal/abi/escape.go @@ -15,3 +15,35 @@ func Escape[T any](x T) T { // as-is. panic("internal/abi.Escape: unreachable (implemented in the compiler)") } + +// EscapeNonString forces v to be on the heap, if v contains a +// non-string pointer. +// +// This is used in hash/maphash.Comparable. We cannot hash pointers +// to local variables on stack, as their addresses might change on +// stack growth. Strings are okay as the hash depends on only the +// content, not the pointer. +// +// This is essentially +// +// if hasNonStringPointers(T) { Escape(v) } +// +// Implemented as a compiler intrinsic. +func EscapeNonString[T any](v T) { panic("intrinsic") } + +// EscapeToResultNonString models a data flow edge from v to the result, +// if v contains a non-string pointer. If v contains only string pointers, +// it returns a copy of v, but is not modeled as a data flow edge +// from the escape analysis's perspective. +// +// This is used in unique.clone, to model the data flow edge on the +// value with strings excluded, because strings are cloned (by +// content). +// +// TODO: probably we should define this as a intrinsic and EscapeNonString +// could just be "heap = EscapeToResultNonString(v)". This way we can model +// an edge to the result but not necessarily heap. +func EscapeToResultNonString[T any](v T) T { + EscapeNonString(v) + return *(*T)(NoEscape(unsafe.Pointer(&v))) +}