Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions builder/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package builder
import (
"errors"
"fmt"
"io/fs"
"os/exec"
"runtime"
"strings"
Expand All @@ -25,20 +26,24 @@ func init() {
// Add the path to a Homebrew-installed LLVM for ease of use (no need to
// manually set $PATH).
if runtime.GOOS == "darwin" {
var prefix string
// The newest LLVM release may still be under Homebrew's unversioned
// "llvm" formula, so that path is tried too.
var prefix, unversionedPrefix string
switch runtime.GOARCH {
case "amd64":
prefix = "/usr/local/opt/llvm@" + llvmMajor + "/bin/"
unversionedPrefix = "/usr/local/opt/llvm/bin/"
case "arm64":
prefix = "/opt/homebrew/opt/llvm@" + llvmMajor + "/bin/"
unversionedPrefix = "/opt/homebrew/opt/llvm/bin/"
default:
// unknown GOARCH
panic(fmt.Sprintf("unknown GOARCH: %s on darwin", runtime.GOARCH))
}
commands["clang"] = append(commands["clang"], prefix+"clang-"+llvmMajor)
commands["ld.lld"] = append(commands["ld.lld"], prefix+"ld.lld")
commands["wasm-ld"] = append(commands["wasm-ld"], prefix+"wasm-ld")
commands["lldb"] = append(commands["lldb"], prefix+"lldb")
commands["clang"] = append(commands["clang"], prefix+"clang-"+llvmMajor, unversionedPrefix+"clang-"+llvmMajor)
commands["ld.lld"] = append(commands["ld.lld"], prefix+"ld.lld", unversionedPrefix+"ld.lld")
commands["wasm-ld"] = append(commands["wasm-ld"], prefix+"wasm-ld", unversionedPrefix+"wasm-ld")
commands["lldb"] = append(commands["lldb"], prefix+"lldb", unversionedPrefix+"lldb")
}
// Add the path for when LLVM was installed with the installer from
// llvm.org, which by default doesn't add LLVM to the $PATH environment
Expand Down Expand Up @@ -66,7 +71,9 @@ func LookupCommand(name string) (string, error) {
for _, cmdName := range commands[name] {
_, err := exec.LookPath(cmdName)
if err != nil {
if errors.Unwrap(err) == exec.ErrNotFound {
// A missing bare command wraps exec.ErrNotFound. A missing
// absolute path surfaces fs.ErrNotExist instead. Skip either.
if errors.Unwrap(err) == exec.ErrNotFound || errors.Is(err, fs.ErrNotExist) {
continue
}
return cmdName, err
Expand Down
97 changes: 97 additions & 0 deletions cgo/cgo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"runtime"
"strings"
"testing"

"github.com/tinygo-org/tinygo/goenv"
)

// Pass -update to go test to update the output of the test files.
Expand Down Expand Up @@ -122,6 +124,101 @@ func TestCGo(t *testing.T) {
}
}

func TestStandardHeaderTypes(t *testing.T) {
const source = `package main
/*
#include <stddef.h>
typedef size_t count_t;
typedef __typeof__(sizeof(0)) expr_size_t;
typedef __typeof__((char *)0 - (char *)0) expr_ptrdiff_t;
expr_size_t byte_count(count_t n);
size_t strlen(const char *s);
*/
import "C"

var _ C.size_t = C.expr_size_t(0)
var _ C.ptrdiff_t = C.expr_ptrdiff_t(0)
var _ C.size_t = C.byte_count(C.count_t(0))
var _ C.size_t = C.strlen(nil)
`
for _, tc := range []struct {
target string
arch string
sizeType string
diffType string
}{
{"armv6m-unknown-unknown-eabi", "arm", "_Cgo_uint", "_Cgo_int"},
{"aarch64-unknown-linux-gnu", "arm64", "_Cgo_ulong", "_Cgo_long"},
{"wasm32-unknown-unknown", "wasm", "_Cgo_ulong", "_Cgo_long"},
} {
t.Run(tc.target, func(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "types.go", source, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
cflags := []string{"--target=" + tc.target}
if resourceDir := goenv.ClangResourceDir(true); resourceDir != "" {
cflags = append(cflags, "-resource-dir="+resourceDir)
}
cgoFiles, _, _, _, _, cgoErrors := Process([]*ast.File{f}, ".", "main", fset, cflags, "linux")
if len(cgoErrors) != 0 {
t.Fatalf("cgo errors: %v", cgoErrors)
}
config := types.Config{
Importer: newSimpleImporter(),
Sizes: types.SizesFor("gc", tc.arch),
}
if _, err := config.Check("", fset, append([]*ast.File{f}, cgoFiles...), nil); err != nil {
t.Fatal(err)
}
wantTypes := map[string]string{
"_Cgo_size_t": tc.sizeType,
"_Cgo_ptrdiff_t": tc.diffType,
"_Cgo_count_t": "_Cgo_size_t",
"_Cgo_expr_size_t": tc.sizeType,
"_Cgo_expr_ptrdiff_t": tc.diffType,
}
ast.Inspect(cgoFiles[0], func(node ast.Node) bool {
spec, ok := node.(*ast.TypeSpec)
if !ok {
return true
}
want, ok := wantTypes[spec.Name.Name]
if !ok {
return true
}
got, ok := spec.Type.(*ast.Ident)
if !ok || got.Name != want {
t.Errorf("%s: got %v, want %s", spec.Name.Name, spec.Type, want)
}
delete(wantTypes, spec.Name.Name)
return true
})
for name := range wantTypes {
t.Errorf("missing type %s", name)
}
})
}
}

func TestUnexposedCanonicalType(t *testing.T) {
const source = `package main
// typedef _BitInt(17) unsupported_t;
import "C"
var _ C.unsupported_t
`
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "types.go", source, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
_, _, _, _, _, cgoErrors := Process([]*ast.File{f}, ".", "main", fset, []string{"--target=armv6m-unknown-unknown-eabi"}, "linux")
if len(cgoErrors) != 1 || !strings.Contains(cgoErrors[0].Error(), "unknown C type: _BitInt(17) (libclang type kind Unexposed)") {
t.Fatalf("unexpected cgo errors: %v", cgoErrors)
}
}

func Test_cgoPackage_isEquivalentAST(t *testing.T) {
fieldA := &ast.Field{Type: &ast.BasicLit{Kind: token.STRING, Value: "a"}}
fieldB := &ast.Field{Type: &ast.BasicLit{Kind: token.STRING, Value: "b"}}
Expand Down
20 changes: 12 additions & 8 deletions cgo/libclang.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ import (
#include <stdlib.h>
#include <stdint.h>

#if LLVM_VERSION_MAJOR >= 23
#define tinygo_CXType_PredefinedSugar CXType_PredefinedSugar
#else
#define tinygo_CXType_PredefinedSugar CXType_Unexposed
#endif

// This struct should be ABI-compatible on all platforms (uintptr_t has the same
// alignment etc. as void*) but does not include void* pointers that are not
// always real pointers.
Expand Down Expand Up @@ -694,6 +700,12 @@ func (f *cgoFile) makeDecayingASTType(typ C.CXType, pos token.Pos) ast.Expr {
// makeASTType return the ast.Expr for the given libclang type. In other words,
// it converts a libclang type to a type in the Go AST.
func (f *cgoFile) makeASTType(typ C.CXType, pos token.Pos) ast.Expr {
if typ.kind == C.CXType_Unexposed || typ.kind == C.tinygo_CXType_PredefinedSugar {
canonical := C.clang_getCanonicalType(typ)
if canonical.kind != C.CXType_Invalid && canonical.kind != C.CXType_Unexposed && canonical.kind != C.tinygo_CXType_PredefinedSugar {
return f.makeASTType(canonical, pos)
}
}
var typeName string
switch typ.kind {
case C.CXType_Char_S, C.CXType_Char_U:
Expand Down Expand Up @@ -804,14 +816,6 @@ func (f *cgoFile) makeASTType(typ C.CXType, pos token.Pos) ast.Expr {
f.addError(pos, fmt.Sprintf("unknown elaborated type (libclang type kind %s)", typeKindSpelling))
typeName = "<unknown>"
}
case C.CXType_Unexposed:
// LLVM 22+ may report certain builtin type aliases (e.g. __size_t)
// as Unexposed. Resolve via the canonical type.
canonical := C.clang_getCanonicalType(typ)
if canonical.kind != C.CXType_Unexposed && canonical.kind != C.CXType_Invalid {
return f.makeASTType(canonical, pos)
}
// If still unexposed, fall through to the error below.
case C.CXType_Record:
cursor := C.tinygo_clang_getTypeDeclaration(typ)
name := getString(C.tinygo_clang_getCursorSpelling(cursor))
Expand Down
2 changes: 1 addition & 1 deletion cgo/libclang_config_llvm20.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !byollvm && !llvm15 && !llvm16 && !llvm17 && !llvm18 && !llvm19 && !llvm21 && !llvm22
//go:build !byollvm && !llvm15 && !llvm16 && !llvm17 && !llvm18 && !llvm19 && !llvm21 && !llvm22 && !llvm23

package cgo

Expand Down
18 changes: 18 additions & 0 deletions cgo/libclang_config_llvm23.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//go:build !byollvm && llvm23

package cgo

// Homebrew may still ship LLVM 23 under the unversioned llvm formula.
// Search both llvm@23 and llvm for headers and libraries.

/*
#cgo linux CFLAGS: -I/usr/include/llvm-23 -I/usr/include/llvm-c-23 -I/usr/lib/llvm-23/include -I/usr/lib64/llvm23/include
#cgo darwin,amd64 CFLAGS: -I/usr/local/opt/llvm@23/include -I/usr/local/opt/llvm/include
#cgo darwin,arm64 CFLAGS: -I/opt/homebrew/opt/llvm@23/include -I/opt/homebrew/opt/llvm/include
#cgo freebsd CFLAGS: -I/usr/local/llvm23/include
#cgo linux LDFLAGS: -L/usr/lib/llvm-23/lib -lclang
#cgo darwin,amd64 LDFLAGS: -L/usr/local/opt/llvm@23/lib -L/usr/local/opt/llvm/lib -lclang
#cgo darwin,arm64 LDFLAGS: -L/opt/homebrew/opt/llvm@23/lib -L/opt/homebrew/opt/llvm/lib -lclang
#cgo freebsd LDFLAGS: -L/usr/local/llvm23/lib -lclang
*/
import "C"
2 changes: 1 addition & 1 deletion compileopts/features_default.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !llvm22 && !llvm14 && !llvm15 && !llvm16 && !llvm17 && !llvm18 && !llvm19
//go:build !llvm14 && !llvm15 && !llvm16 && !llvm17 && !llvm18 && !llvm19 && !llvm22 && !llvm23

package compileopts

Expand Down
4 changes: 2 additions & 2 deletions compileopts/features_llvm22.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//go:build llvm22
//go:build llvm22 || llvm23

package compileopts

import "strings"

// patchFeatures applies LLVM-version-specific feature name mappings.
// LLVM 22 renamed several Xtensa target features.
// LLVM 22 renamed several Xtensa target features, assumed unchanged in 23.
func patchFeatures(features string) string {
// Xtensa feature renames in LLVM 22:
// atomctl → (removed, no direct replacement)
Expand Down
2 changes: 1 addition & 1 deletion compileopts/triple_llvm22.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build llvm22
//go:build llvm22 || llvm23

package compileopts

Expand Down
2 changes: 1 addition & 1 deletion compileopts/triple_pre22.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !llvm22
//go:build !llvm22 && !llvm23

package compileopts

Expand Down
32 changes: 30 additions & 2 deletions compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -561,12 +561,40 @@ var capturesNoneAttrRe = regexp.MustCompile(`\b(readonly|readnone|writeonly|nonn
// llvm.lifetime.start/end call or declaration, which LLVM 22 removed.
var lifetimeSizeArgRe = regexp.MustCompile(`(@llvm\.lifetime\.(?:start|end)\.p0\()i64(?: immarg| \d+), `)

// nosyncAttrRe matches the standalone 'nosync' function attribute, which
// LLVM 23 started inferring for llvm.memcpy/llvm.memmove declarations.
var nosyncAttrRe = regexp.MustCompile(`\bnosync\s+`)

// guidAttachmentRe matches the inline '!guid !N' metadata LLVM 23 attaches
// to every GlobalValue once a pass builds a module summary. A global's
// attachment is comma separated, a function's is space separated.
var guidAttachmentRe = regexp.MustCompile(`,?\s*!guid\s+!\d+`)

// guidMetadataRe matches the standalone metadata node backing a '!guid'
// attachment.
var guidMetadataRe = regexp.MustCompile(`(?m)^!\d+ = !\{i64 -?\d+\}\n?`)

// targetMemAttrRe matches the target_mem entries of a memory() attribute.
// Older LLVM numbered them target_mem0/target_mem1; LLVM 23 emits one
// unnumbered target_mem instead.
var targetMemAttrRe = regexp.MustCompile(`(,\s*target_mem\d*: none)+`)

// nofreeNocaptureRe matches LLVM 23's added 'nofree' on a parameter that's
// already 'nocapture'.
var nofreeNocaptureRe = regexp.MustCompile(`\bnofree\s+nocapture\b`)

// normalizeCapturesAttr rewrites LLVM 21+'s 'captures(none)' attribute back
// to the pre-LLVM21 'nocapture' spelling and position, so golden IR files
// written against LLVM <21 keep matching.
// to the pre-LLVM21 'nocapture' spelling, and drops LLVM 23's added
// 'nosync' attribute and '!guid' metadata, so golden files keep matching
// across versions.
func normalizeCapturesAttr(s string) string {
s = capturesNoneAttrRe.ReplaceAllString(s, "nocapture $1")
s = strings.ReplaceAll(s, "captures(none)", "nocapture")
s = nosyncAttrRe.ReplaceAllString(s, "")
s = guidAttachmentRe.ReplaceAllString(s, "")
s = guidMetadataRe.ReplaceAllString(s, "")
s = targetMemAttrRe.ReplaceAllString(s, "")
s = nofreeNocaptureRe.ReplaceAllString(s, "nocapture")
return s
}

Expand Down
5 changes: 5 additions & 0 deletions compiler/symbol.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,11 @@ func (c *compilerContext) parsePragmas(info *functionInfo, f *ssa.Function) {
}

info.linkName = parts[1]
if info.linkName == "llvm.returnaddress" && llvmutil.Version() >= 23 {
// LLVM 23 requires the pointer-type suffix on this
// intrinsic's mangled name.
info.linkName = "llvm.returnaddress.p0"
}
info.wasmName = info.linkName
info.exported = true
case "//go:interrupt":
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ require (
golang.org/x/tools v0.47.0
gopkg.in/yaml.v2 v2.4.0
tinygo.org/x/espflasher v0.8.1
tinygo.org/x/go-llvm v0.0.0-20260721072906-185673ef46a5
tinygo.org/x/go-llvm v0.0.0-20261002181809-2c9e91a80029
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
tinygo.org/x/espflasher v0.8.1 h1:Vp+xA16af9NKVOOSJCcrJHqPSLDCpbfCd/TG3x449ks=
tinygo.org/x/espflasher v0.8.1/go.mod h1:YLkCtOCz6gdrbueTi6uZbGPSYduTYt62JdNGmiGxgc0=
tinygo.org/x/go-llvm v0.0.0-20260721072906-185673ef46a5 h1:0PKRhM1INWAi7PdIng1BHMPTDaRcOmv1UymdTgZ76Jo=
tinygo.org/x/go-llvm v0.0.0-20260721072906-185673ef46a5/go.mod h1:GFbusT2VTA4I+l4j80b17KFK+6whv69Wtny5U+T8RR0=
tinygo.org/x/go-llvm v0.0.0-20261002181809-2c9e91a80029 h1:B6MwkUR6/LkeeRHhDdjS4PpHR8RCvwzgv0n2JmHjyzc=
tinygo.org/x/go-llvm v0.0.0-20261002181809-2c9e91a80029/go.mod h1:GFbusT2VTA4I+l4j80b17KFK+6whv69Wtny5U+T8RR0=
42 changes: 42 additions & 0 deletions interp/branch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package interp

// This file contains all decoding of LLVM branch instructions, so that changes
// in how LLVM represents them only need to be handled here.

import "tinygo.org/x/go-llvm"

// opBr is the opcode interp uses for every branch instruction, conditional or
// not. The two forms are told apart by their number of operands.
const opBr = llvm.Br

// isBranch returns whether inst is a (conditional or unconditional) br
// instruction.
func isBranch(inst llvm.Value) bool {
return !inst.IsABranchInst().IsNil()
}

// isCondBranch returns whether inst is a conditional br instruction.
func isCondBranch(inst llvm.Value) bool {
return isBranch(inst) && inst.SuccessorsCount() == 2
}

// isUncondBranch returns whether inst is an unconditional br instruction.
func isUncondBranch(inst llvm.Value) bool {
return isBranch(inst) && inst.SuccessorsCount() == 1
}

// branchCondition returns the i1 condition of a conditional br instruction.
func branchCondition(inst llvm.Value) llvm.Value {
// The bindings lack LLVMGetCondition, so read the operand directly.
return inst.Operand(0)
}

// branchThen and branchElse return the destination of a conditional br
// instruction when the condition is true or false, respectively. Successors
// are used instead of operands because the successor order is stable while
// the operand order is not (it is reversed in LLVM 22 and older).
func branchThen(inst llvm.Value) llvm.BasicBlock { return inst.Successor(0) }
func branchElse(inst llvm.Value) llvm.BasicBlock { return inst.Successor(1) }

// branchTarget returns the destination of an unconditional br instruction.
func branchTarget(inst llvm.Value) llvm.BasicBlock { return inst.Successor(0) }
Loading
Loading