Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
253ae7c
feat: reduce calling libclang mutiple time
MeteorsLiu Jun 5, 2025
19846fb
test: add commondir test case 3
MeteorsLiu Jun 5, 2025
bcc3e2e
test: remove test case 3
MeteorsLiu Jun 5, 2025
ec3dfe8
fix: use MM to retrieve interface
MeteorsLiu Jun 5, 2025
1a3a4f4
fix: mm output parse
MeteorsLiu Jun 5, 2025
4c9ccfa
fix: mm output parse
MeteorsLiu Jun 5, 2025
277bc64
add test
MeteorsLiu Jun 5, 2025
f22a90d
add test
MeteorsLiu Jun 5, 2025
2f4085b
add test
MeteorsLiu Jun 5, 2025
8b8b9eb
add test
MeteorsLiu Jun 5, 2025
39ee6de
add test
MeteorsLiu Jun 5, 2025
0494289
add test
MeteorsLiu Jun 5, 2025
aa853a5
add test
MeteorsLiu Jun 5, 2025
2658889
add test
MeteorsLiu Jun 5, 2025
8ae3077
add test
MeteorsLiu Jun 5, 2025
fbcdd58
add test
MeteorsLiu Jun 5, 2025
726df0c
add test
MeteorsLiu Jun 5, 2025
1100394
add test
MeteorsLiu Jun 5, 2025
7844d98
add test
MeteorsLiu Jun 5, 2025
ba356a3
feat: implement trie for longest common prefix
MeteorsLiu Jun 6, 2025
9cac662
test: add more abs tests
MeteorsLiu Jun 6, 2025
e25406b
chore: remove println
MeteorsLiu Jun 6, 2025
1c1c16f
test: fix test
MeteorsLiu Jun 6, 2025
3cf470f
Merge branch 'main' of https://github.com/goplus/llcppg into trie
MeteorsLiu Jun 6, 2025
bc22861
merge
MeteorsLiu Jun 6, 2025
c8484de
chore: fix namespace
MeteorsLiu Jun 6, 2025
1522a10
merge
MeteorsLiu Jun 6, 2025
3d303d1
Merge branch 'trie' into config/optimize-pkghfile
MeteorsLiu Jun 6, 2025
e6de029
fix: change contains logic
MeteorsLiu Jun 6, 2025
f571c18
Merge branch 'trie' into config/optimize-pkghfile
MeteorsLiu Jun 6, 2025
cc75254
fix: use trie to filter out non-interface header files
MeteorsLiu Jun 6, 2025
3776c37
fix: contains logic
MeteorsLiu Jun 6, 2025
690b3f8
test: add more tests
MeteorsLiu Jun 6, 2025
8f81a2b
chore: rename Contains
MeteorsLiu Jun 6, 2025
1102c98
test: remove duplicated test
MeteorsLiu Jun 6, 2025
d429ac9
Merge branch 'trie' into config/optimize-pkghfile
MeteorsLiu Jun 6, 2025
a766abb
feat: use trie to replace commonDir
MeteorsLiu Jun 6, 2025
1315fe9
add test
MeteorsLiu Jun 6, 2025
7a11f7c
add test
MeteorsLiu Jun 6, 2025
3780339
feat: use DFS to scan the longest common prefix
MeteorsLiu Jun 6, 2025
bad7b23
Merge branch 'trie' into config/optimize-pkghfile
MeteorsLiu Jun 6, 2025
c0263ed
chore: rename IsSubset
MeteorsLiu Jun 6, 2025
2ad3a37
Merge branch 'trie' into config/optimize-pkghfile
MeteorsLiu Jun 6, 2025
8c25072
fix: trie name
MeteorsLiu Jun 6, 2025
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
112 changes: 78 additions & 34 deletions _xtool/internal/header/header.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package header

import (
"fmt"
"io"
"maps"
"os"
"path/filepath"
"slices"
"sort"
"strings"

"github.com/goplus/lib/c/clang"
Expand Down Expand Up @@ -41,75 +46,84 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo {
}
defer os.Remove(outfile.Name())

inters := make(map[string]struct{})
others := []string{} // impl & third
for _, f := range includes {
content := "#include <" + f + ">"
index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{
File: content,
Temp: true,
Args: args,
})
if err != nil {
panic(err)
}
clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) {
if len(incins) == 1 {
filename := filepath.Clean(clang.GoString(inced.FileName()))
info.Inters = append(info.Inters, filename)
inters[filename] = struct{}{}
}
})
unit.Dispose()
index.Dispose()
mmOutput, err := os.CreateTemp("", "mmoutput_*")
if err != nil {
panic(err)
}
defer os.Remove(mmOutput.Name())

includeTrie := NewTrie(WithReversePathSegmenter())

for _, inc := range includes {
includeTrie.Insert(inc)
args = append(args, fmt.Sprintf("--no-system-header-prefix=%s", inc))
}

args = append(args, "-MM", "-MF", mmOutput.Name())

clangtool.ComposeIncludes(includes, outfile.Name())
index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{
File: outfile.Name(),
Temp: false,
Args: args,
})

defer unit.Dispose()
defer index.Dispose()
if err != nil {
panic(err)
}

var others []string
inters, longestPrefix := RetrieveInterfaceFromMM(outfile.Name(), mmOutput, includeTrie)

clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) {
// not in the first level include maybe impl or third hfile
filename := filepath.Clean(clang.GoString(inced.FileName()))
_, inter := inters[filename]
if len(incins) > 1 && !inter {

// skip the composed header
if filename == outfile.Name() {
return
}

if _, isInterface := inters[filename]; !isInterface {
others = append(others, filename)
}
})

if mix {
info.Thirds = others
return info
}
info.Inters = slices.Collect(maps.Keys(inters))

root, err := filepath.Abs(commonParentDir(info.Inters))
absLongestPrefix, err := filepath.Abs(longestPrefix)
if err != nil {
panic(err)
}
for _, f := range others {
file, err := filepath.Abs(f)

fmt.Fprintln(os.Stderr, "tttttt", longestPrefix, CommonParentDir(info.Inters))

for _, filename := range others {
if mix {
info.Thirds = append(info.Thirds, filename)
continue
}
filePath, err := filepath.Abs(filename)
if err != nil {
panic(err)
}
if strings.HasPrefix(file, root) {
info.Impls = append(info.Impls, f)
if strings.HasPrefix(filePath, absLongestPrefix) {
info.Impls = append(info.Impls, filename)
} else {
info.Thirds = append(info.Thirds, f)
info.Thirds = append(info.Thirds, filename)
}
}

sort.Strings(info.Inters)

return info
}

// commonParentDir finds the longest common parent directory path for a given slice of paths.
// For example, given paths ["/a/b/c/d", "/a/b/e/f"], it returns "/a/b".
func commonParentDir(paths []string) string {
func CommonParentDir(paths []string) string {
if len(paths) == 0 {
return ""
}
Expand All @@ -128,3 +142,33 @@ func commonParentDir(paths []string) string {
}
return filepath.Dir(paths[0])
}

func RetrieveInterfaceFromMM(
composedHeaderFileName string,
mmOutput *os.File,
includeTrie *Trie,
) (interfaceMap map[string]struct{}, prefix string) {
fileName := strings.TrimSuffix(filepath.Base(composedHeaderFileName), ".h")

interfaceMap = make(map[string]struct{})

content, _ := io.ReadAll(mmOutput)

mmTrie := NewTrie()

for _, line := range strings.Fields(string(content)) {
// skip composed header file
if strings.Contains(line, fileName) || line == `\` {
continue
}
headerFile := filepath.Clean(line)

if includeTrie.IsOnSameBranch(headerFile) {
mmTrie.Insert(headerFile)

interfaceMap[headerFile] = struct{}{}
}
}
prefix = mmTrie.LongestPrefix()
return
}
175 changes: 175 additions & 0 deletions _xtool/internal/header/header_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ package header_test

import (
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"

"github.com/goplus/lib/c/clang"
clangutils "github.com/goplus/llcppg/_xtool/internal/clang"
"github.com/goplus/llcppg/_xtool/internal/clangtool"
"github.com/goplus/llcppg/_xtool/internal/header"
llconfig "github.com/goplus/llcppg/config"
)
Expand Down Expand Up @@ -73,3 +78,173 @@ func TestPkgHfileInfo(t *testing.T) {
})
}
}

func TestLongestPrefix(t *testing.T) {
testCases := []struct {
name string
strs []string
want string
}{
{
name: "empty string 1",
strs: []string{},
want: "",
},
{
name: "empty string 2",
strs: []string{"", ""},
want: ".",
},
{
name: "one empty string(b)",
strs: []string{"/a", ""},
want: "",
},
{
name: "one empty string(a)",
strs: []string{"", "/a"},

want: "",
},
// FIXME: substring bug
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Note is recommended to use "MARKER(uid): note body" format.

Details

lint 解释

这个lint结果提示在注释中使用“MARKER(uid): note body”格式是一个建议。这意味着在编写注释时,应该遵循这种特定的格式来提高代码的可读性和一致性。

错误用法

以下是一个错误的示例,展示了不正确的注释格式:

// 这是一个错误的注释格式

正确用法

以下是一个正确的示例,展示了符合建议的注释格式:

// MARKER(12345): 这是一个正确的注释格式

💡 以上内容由 AI 辅助生成,如有疑问欢迎反馈交流

// {
// name: "b is substring of a",
// strs: []string{"/usr/a/b", "/usr/a"},
// want: "/usr/a",
// },
// {
// name: "a is substring of b",
// strs: []string{"/usr/c", "/usr/c/b"},
// want: "/usr/c",
// },
{
name: "normal case 1",
strs: []string{"testdata/hfile/temp1.h", "testdata/thirdhfile/third.h"},
want: "testdata",
},
{
name: "normal case 2",
strs: []string{"testdata/hfile/temp1.h", "testdata/hfile/third.h"},

want: "testdata/hfile",
},
// FIXME: absolute path
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Note is recommended to use "MARKER(uid): note body" format.

Details

lint 解释

这个lint结果提示在注释中使用“MARKER(uid): note body”格式是一个推荐的做法。这意味着在编写注释时,应该遵循这种特定的格式来提高代码的可读性和一致性。

错误用法

以下是一个错误的示例,展示了不正确的注释格式:

// 这是一个错误的注释格式

正确用法

以下是一个正确的示例,展示了符合推荐格式的注释:

// MARKER(12345): 这是一个正确的注释格式

💡 以上内容由 AI 辅助生成,如有疑问欢迎反馈交流

// {
// name: "normal case 3",
// strs: []string{"/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON/cJSON.h", "/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON.h", "/opt/homebrew/Cellar/cjson/1.7.18/include/zlib/zlib.h"},
// want: "/opt/homebrew/Cellar/cjson/1.7.18/include",
// },
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := header.CommonParentDir(tc.strs); got != tc.want {
t.Fatalf("unexpected longest prefix: want %s got %s", tc.want, got)
}
})
}
}

func benchmarkFn(fn func()) time.Duration {
now := time.Now()

fn()

return time.Since(now)
}

func TestBenchmarkPkgHfileInfo(t *testing.T) {
include := []string{"temp1.h", "temp2.h"}
cflags := []string{"-I./testdata/hfile", "-I./testdata/thirdhfile"}
t1 := benchmarkFn(func() {
for i := 0; i < 100; i++ {
pkgHfileInfo(include, cflags, false)
}
})

t2 := benchmarkFn(func() {
for i := 0; i < 100; i++ {
header.PkgHfileInfo(include, cflags, false)
}
})

fmt.Println("old PkgHfileInfo elapsed: ", t1, "new PkgHfileInfo elasped: ", t2)
}

func pkgHfileInfo(includes []string, args []string, mix bool) *header.PkgHfilesInfo {
info := &header.PkgHfilesInfo{
Inters: []string{},
Impls: []string{},
Thirds: []string{},
}
outfile, err := os.CreateTemp("", "compose_*.h")
if err != nil {
panic(err)
}
defer os.Remove(outfile.Name())

inters := make(map[string]struct{})
others := []string{} // impl & third
for _, f := range includes {
content := "#include <" + f + ">"
index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{
File: content,
Temp: true,
Args: args,
})
if err != nil {
panic(err)
}
clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) {
if len(incins) == 1 {
filename := filepath.Clean(clang.GoString(inced.FileName()))
info.Inters = append(info.Inters, filename)
inters[filename] = struct{}{}
}
})
unit.Dispose()
index.Dispose()
}

clangtool.ComposeIncludes(includes, outfile.Name())
index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{
File: outfile.Name(),
Temp: false,
Args: args,
})
defer unit.Dispose()
defer index.Dispose()
if err != nil {
panic(err)
}
clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) {
// not in the first level include maybe impl or third hfile
filename := filepath.Clean(clang.GoString(inced.FileName()))
_, inter := inters[filename]
if len(incins) > 1 && !inter {
others = append(others, filename)
}
})

if mix {
info.Thirds = others
return info
}

root, err := filepath.Abs(header.CommonParentDir(info.Inters))
if err != nil {
panic(err)
}
for _, f := range others {
file, err := filepath.Abs(f)
if err != nil {
panic(err)
}
if strings.HasPrefix(file, root) {
info.Impls = append(info.Impls, f)
} else {
info.Thirds = append(info.Thirds, f)
}
}
return info
}
1 change: 1 addition & 0 deletions _xtool/internal/header/testdata/hfile/temp1.h
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
#include "tempimpl.h"
#include "temp2.h"
#include <third.h>
Loading
Loading