From ad00b694406bdead63f96f6a17d7493d06f02edc Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Wed, 26 Aug 2026 19:51:29 +0200 Subject: [PATCH 01/12] feat(go-models): add model support to core --- .../org/opentaint/ir/go/client/GoIRClient.kt | 1 + .../opentaint/ir/go/client/GoIRLoadConfig.kt | 3 + .../ir/go/test/features/ModelTests.kt | 97 ++ .../go/go-ssa-server/build_program_test.go | 810 ++++++++++++- core/opentaint-ir/go/go-ssa-server/builder.go | 42 +- .../go/go-ssa-server/model_merge.go | 1053 +++++++++++++++++ .../go/go-ssa-server/model_workspace.go | 89 ++ .../go/go-ssa-server/serializer.go | 20 +- core/opentaint-ir/go/go-ssa-server/server.go | 74 +- core/opentaint-ir/go/proto/goir/service.proto | 1 + .../ir/approximation/Approximations.kt | 101 +- .../annotation/ApproximationAnnotations.kt | 11 + .../java/opentaint/java/lang/Integer.java | 17 + .../target/KotlinClassName.java | 10 + .../ir/approximations/target/KotlinNames.java | 14 + .../approximations/approx/IntegerApprox.java | 21 - .../legacy/LegacyTargetApprox.java | 11 + .../ir/approximations/target/KotlinClass.kt} | 17 +- .../ir/approximations/ApproximationsTest.kt | 51 +- .../ir/approximations/target/KotlinNames.kt | 16 + .../opentaint/go/sast/project/AnalysisCtx.kt | 9 +- .../sast/project/GoProjectAnalysisOptions.kt | 2 + .../go/sast/project/GoProjectAnalyzer.kt | 4 +- .../go/sast/project/GoTestProjectAnalyzer.kt | 3 +- .../jvm/sast/runner/ProjectAnalyzerRunner.kt | 9 +- 25 files changed, 2414 insertions(+), 72 deletions(-) create mode 100644 core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt create mode 100644 core/opentaint-ir/go/go-ssa-server/model_merge.go create mode 100644 core/opentaint-ir/go/go-ssa-server/model_workspace.go create mode 100644 core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/java/lang/Integer.java create mode 100644 core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/org/opentaint/ir/approximations/target/KotlinClassName.java create mode 100644 core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/org/opentaint/ir/approximations/target/KotlinNames.java delete mode 100644 core/opentaint-ir/opentaint-ir-approximations/src/test/java/org/opentaint/ir/approximations/approx/IntegerApprox.java create mode 100644 core/opentaint-ir/opentaint-ir-approximations/src/test/java/org/opentaint/ir/approximations/legacy/LegacyTargetApprox.java rename core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/{org/opentaint/ir/approximations/approx/KotlinClassApprox.kt => opentaint/org/opentaint/ir/approximations/target/KotlinClass.kt} (72%) create mode 100644 core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/target/KotlinNames.kt diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRClient.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRClient.kt index f79c449838..997d90b256 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRClient.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRClient.kt @@ -41,6 +41,7 @@ class GoIRClient : AutoCloseable { .addAllProjectModulePaths(config.projectModules) .setInstantiateGenerics(config.instantiateGenerics) .setSanityCheck(config.sanityCheck) + .addAllModelDirs(config.modelDirs.map { it.toAbsolutePath().toString() }) .build() val totalStart = System.nanoTime() diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRLoadConfig.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRLoadConfig.kt index 62dc61bcd7..cbf69ff2eb 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRLoadConfig.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRLoadConfig.kt @@ -1,5 +1,7 @@ package org.opentaint.ir.go.client +import java.nio.file.Path + enum class GoIRLoadMode { FULL, PROJECT } data class GoIRLoadConfig( @@ -8,4 +10,5 @@ data class GoIRLoadConfig( val projectModules: Set = emptySet(), val instantiateGenerics: Boolean = true, val sanityCheck: Boolean = true, + val modelDirs: List = emptyList(), ) diff --git a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt new file mode 100644 index 0000000000..0e7fdec906 --- /dev/null +++ b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt @@ -0,0 +1,97 @@ +package org.opentaint.ir.go.test.features + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.opentaint.ir.go.client.GoIRLoadConfig +import org.opentaint.ir.go.client.GoIRLoadMode +import org.opentaint.ir.go.expr.GoIRFieldAddrExpr +import org.opentaint.ir.go.ext.findExpressions +import org.opentaint.ir.go.inst.GoIRReturn +import org.opentaint.ir.go.test.GoIRTestBuilder +import org.opentaint.ir.go.test.GoIRTestExtension +import org.opentaint.ir.go.value.GoIRParameterValue +import java.nio.file.Files +import java.nio.file.Path + +@ExtendWith(GoIRTestExtension::class) +class ModelTests { + @Test + fun `model body replaces dependency body after client deserialization`(builder: GoIRTestBuilder) { + val project = Files.createTempDirectory("goir-model-project") + project.resolve("go.mod").write("module example.com/app\ngo 1.22\n") + project.resolve("main.go").write( + "package main\nimport \"strings\"\nfunc Upper(value string) string { return strings.ToUpper(value) }\n", + ) + + val model = Files.createTempDirectory("goir-model") + model.resolve("go.mod").write("module opentaint\ngo 1.25\n") + val modelPackage = model.resolve("strings") + Files.createDirectories(modelPackage) + modelPackage.resolve("model.go").write( + "package strings\nfunc ToUpper(value string) string { return value }\n", + ) + + val program = builder.buildFromDir( + project, + GoIRLoadConfig(mode = GoIRLoadMode.PROJECT, modelDirs = listOf(model)), + ) + + assertThat(program.findPackage("opentaint/strings")).isNull() + val function = program.findPackage("strings")!!.functions.single { it.name == "ToUpper" } + assertThat(function.bodyAvailable).isTrue() + val returned = (function.body!!.blocks.single().terminator as GoIRReturn).results.single() as GoIRParameterValue + assertThat(returned.paramIndex).isZero() + assertThat(returned.type).isEqualTo(function.params.single().type) + } + + @Test + fun `partial model adds fields and methods after client deserialization`(builder: GoIRTestBuilder) { + val project = Files.createTempDirectory("goir-model-fields-project") + project.resolve("go.mod").write("module example.com/fields\ngo 1.22\n") + project.resolve("fields.go").write( + """ + package fields + type Box struct { Original string } + func (b *Box) Put(value string) { b.Original = "original" } + func (b *Box) Keep(value string) string { return "original" } + """.trimIndent(), + ) + + val model = Files.createTempDirectory("goir-model-fields") + model.resolve("go.mod").write("module opentaint\ngo 1.25\n") + val modelPackage = model.resolve("example.com/fields") + Files.createDirectories(modelPackage) + modelPackage.resolve("model.go").write( + """ + package fields + type Box struct { Shadow string } + func (b *Box) Put(value string) { b.Shadow = value } + func (b *Box) Helper(value string) string { + b.Shadow = value + return b.Shadow + } + """.trimIndent(), + ) + + val program = builder.buildFromDir( + project, + GoIRLoadConfig(mode = GoIRLoadMode.PROJECT, modelDirs = listOf(model)), + ) + + val targetPackage = program.findPackage("example.com/fields")!! + val box = targetPackage.findNamedType("Box")!! + assertThat(box.fields.map { it.name to it.index }) + .containsExactly("Original" to 0, "Shadow" to 1) + assertThat(box.pointerMethods.map { it.name }).contains("Put", "Keep", "Helper") + + val put = box.pointerMethods.single { it.name == "Put" } + val shadowAccess = put.findExpressions().single { it.fieldName == "Shadow" } + assertThat(shadowAccess.fieldIndex).isEqualTo(1) + assertThat(box.pointerMethods.single { it.name == "Keep" }.bodyAvailable).isTrue() + } + + private fun Path.write(content: String) { + toFile().writeText(content) + } +} diff --git a/core/opentaint-ir/go/go-ssa-server/build_program_test.go b/core/opentaint-ir/go/go-ssa-server/build_program_test.go index 57cf40a5a3..19babbda56 100644 --- a/core/opentaint-ir/go/go-ssa-server/build_program_test.go +++ b/core/opentaint-ir/go/go-ssa-server/build_program_test.go @@ -6,6 +6,7 @@ import ( "net" "os" "path/filepath" + "strings" "testing" "google.golang.org/grpc" @@ -27,7 +28,7 @@ func writeTinyModule(t *testing.T) string { return dir } -func runBuild(t *testing.T, req *pb.BuildProgramRequest) ([]*pb.ProtoPackage, map[int32]bool) { +func runBuildProgram(t *testing.T, req *pb.BuildProgramRequest) *pb.ProtoProgram { lis := bufconn.Listen(1 << 20) srv := grpc.NewServer() pb.RegisterGoSSAServiceServer(srv, NewGoSSAServer("test", "test")) @@ -49,8 +50,7 @@ func runBuild(t *testing.T, req *pb.BuildProgramRequest) ([]*pb.ProtoPackage, ma if err != nil { t.Fatal(err) } - var pkgs []*pb.ProtoPackage - bodies := map[int32]bool{} + program := &pb.ProtoProgram{} for { resp, err := stream.Recv() if err != nil { @@ -61,21 +61,59 @@ func runBuild(t *testing.T, req *pb.BuildProgramRequest) ([]*pb.ProtoPackage, ma } switch p := resp.Payload.(type) { case *pb.BuildProgramResponse_Program: - pkgs = append(pkgs, p.Program.Packages...) - for _, b := range p.Program.FunctionBodies { - bodies[b.FunctionId] = true - } + program = p.Program case *pb.BuildProgramResponse_PackageDef: - pkgs = append(pkgs, p.PackageDef) + program.Packages = append(program.Packages, p.PackageDef) case *pb.BuildProgramResponse_FunctionBody: - bodies[p.FunctionBody.FunctionId] = true + program.FunctionBodies = append(program.FunctionBodies, p.FunctionBody) case *pb.BuildProgramResponse_Error: if p.Error.Fatal { t.Fatalf("fatal: %s", p.Error.Message) } } } - return pkgs, bodies + return program +} + +func runBuild(t *testing.T, req *pb.BuildProgramRequest) ([]*pb.ProtoPackage, map[int32]bool) { + program := runBuildProgram(t, req) + bodies := make(map[int32]bool, len(program.FunctionBodies)) + for _, body := range program.FunctionBodies { + bodies[body.FunctionId] = true + } + return program.Packages, bodies +} + +func runBuildFatal(t *testing.T, req *pb.BuildProgramRequest) string { + t.Helper() + lis := bufconn.Listen(1 << 20) + srv := grpc.NewServer() + pb.RegisterGoSSAServiceServer(srv, NewGoSSAServer("test", "test")) + go srv.Serve(lis) + defer srv.Stop() + conn, err := grpc.NewClient("passthrough:///bufnet", + grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) }), + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + stream, err := pb.NewGoSSAServiceClient(conn).BuildProgram(context.Background(), req) + if err != nil { + t.Fatal(err) + } + for { + resp, recvErr := stream.Recv() + if recvErr == io.EOF { + t.Fatal("expected a fatal model validation error") + } + if recvErr != nil { + t.Fatalf("expected a model validation error, got stream error: %v", recvErr) + } + if failure, ok := resp.Payload.(*pb.BuildProgramResponse_Error); ok && failure.Error.Fatal { + return failure.Error.Message + } + } } func findPkg(pkgs []*pb.ProtoPackage, path string) *pb.ProtoPackage { @@ -134,3 +172,755 @@ func TestBuildProgram_FullMode_StreamsStdlibBodies(t *testing.T) { t.Fatal("FULL mode streamed no stdlib bodies") } } + +func writeStringsModel(t *testing.T, packageName string) string { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { + t.Fatal(err) + } + packageDir := filepath.Join(dir, "strings") + if err := os.MkdirAll(packageDir, 0o755); err != nil { + t.Fatal(err) + } + src := "package " + packageName + "\nfunc ToUpper(s string) string { return s }\n" + if err := os.WriteFile(filepath.Join(packageDir, "model.go"), []byte(src), 0o644); err != nil { + t.Fatal(err) + } + return dir +} + +func TestBuildProgram_GoModelReplacesTargetBody(t *testing.T) { + dir := writeTinyModule(t) + modelDir := writeStringsModel(t, "strings") + program := runBuildProgram(t, &pb.BuildProgramRequest{ + WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, + }) + + if findPkg(program.Packages, "opentaint/strings") != nil { + t.Fatal("model-addressed package leaked into the merged program") + } + stringsPkg := findPkg(program.Packages, "strings") + if stringsPkg == nil { + t.Fatal("strings package missing") + } + var target *pb.ProtoFunction + for _, fn := range stringsPkg.Functions { + if fn.Name == "ToUpper" { + target = fn + break + } + } + if target == nil { + t.Fatal("strings.ToUpper missing") + } + var body *pb.ProtoFunctionBody + for _, candidate := range program.FunctionBodies { + if candidate.FunctionId == target.Id { + body = candidate + break + } + } + if body == nil { + t.Fatal("model body was not attached to strings.ToUpper") + } + if len(body.Blocks) != 1 || len(body.Blocks[0].Instructions) == 0 { + t.Fatalf("unexpected model body: %+v", body) + } + ret := body.Blocks[0].Instructions[len(body.Blocks[0].Instructions)-1].GetReturnInst() + if ret == nil || len(ret.Results) != 1 || ret.Results[0].GetParamIndex() != 0 { + t.Fatalf("strings.ToUpper model does not return its parameter: %+v", ret) + } +} + +func TestBuildProgram_GoModelPackageNameMustMatchTarget(t *testing.T) { + dir := writeTinyModule(t) + modelDir := writeStringsModel(t, "modelstrings") + message := runBuildFatal(t, &pb.BuildProgramRequest{ + WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, + }) + if !strings.Contains(message, "package name \"modelstrings\", want \"strings\"") { + t.Fatalf("unexpected validation error: %s", message) + } +} + +func TestBuildProgram_GoModelFunctionSignatureMustMatchTarget(t *testing.T) { + dir := writeTinyModule(t) + modelDir := writeStringsModel(t, "strings") + modelSource := "package strings\nfunc ToUpper(value int) int { return value }\n" + if err := os.WriteFile(filepath.Join(modelDir, "strings", "model.go"), []byte(modelSource), 0o644); err != nil { + t.Fatal(err) + } + + message := runBuildFatal(t, &pb.BuildProgramRequest{ + WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, + }) + if !strings.Contains(message, "strings.ToUpper has a different signature") { + t.Fatalf("unexpected validation error: %s", message) + } +} + +func TestBuildProgram_GoPackageCannotBeModeledTwice(t *testing.T) { + dir := writeTinyModule(t) + firstModel := writeStringsModel(t, "strings") + secondModel := writeStringsModel(t, "strings") + + message := runBuildFatal(t, &pb.BuildProgramRequest{ + WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{firstModel, secondModel}, + }) + if !strings.Contains(message, "Go package \"strings\" is modeled more than once") { + t.Fatalf("unexpected validation error: %s", message) + } +} + +func TestBuildProgram_GoModelDoesNotReplacePackageInitializer(t *testing.T) { + projectDir := t.TempDir() + if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte("module example.com/initmodel\ngo 1.22\n"), 0o644); err != nil { + t.Fatal(err) + } + projectSource := "package initmodel\nvar Value string\nfunc init() { Value = \"original\" }\n" + if err := os.WriteFile(filepath.Join(projectDir, "init.go"), []byte(projectSource), 0o644); err != nil { + t.Fatal(err) + } + + modelDir := t.TempDir() + if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { + t.Fatal(err) + } + modelPackageDir := filepath.Join(modelDir, "example.com", "initmodel") + if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { + t.Fatal(err) + } + modelSource := "package initmodel\nvar Value string\nfunc init() { Value = \"model\" }\n" + if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { + t.Fatal(err) + } + + program := runBuildProgram(t, &pb.BuildProgramRequest{ + WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, + }) + targetPackage := findPkg(program.Packages, "example.com/initmodel") + if targetPackage == nil || targetPackage.InitFunctionId == 0 { + t.Fatalf("target package initializer missing: %+v", targetPackage) + } + var sourceInitializerID int32 + for _, function := range targetPackage.Functions { + if function.Name == "init#1" { + sourceInitializerID = function.Id + break + } + } + if sourceInitializerID == 0 { + t.Fatalf("target source initializer missing: %+v", targetPackage) + } + for _, body := range program.FunctionBodies { + if body.FunctionId != sourceInitializerID { + continue + } + var sawOriginal, sawModel bool + for _, block := range body.Blocks { + for _, instruction := range block.Instructions { + store := instruction.GetStore() + if store == nil || store.Val.GetConstVal() == nil { + continue + } + sawOriginal = sawOriginal || store.Val.GetConstVal().GetStringValue() == "original" + sawModel = sawModel || store.Val.GetConstVal().GetStringValue() == "model" + } + } + if !sawOriginal || sawModel { + t.Fatalf("package initializer was replaced by model: original=%t model=%t", sawOriginal, sawModel) + } + return + } + t.Fatal("target package initializer body missing") +} + +func TestBuildProgram_GoModelPackageRequiresOpentaintPrefix(t *testing.T) { + dir := writeTinyModule(t) + modelDir := t.TempDir() + if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module models\ngo 1.25\n"), 0o644); err != nil { + t.Fatal(err) + } + packageDir := filepath.Join(modelDir, "strings") + if err := os.MkdirAll(packageDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(packageDir, "model.go"), []byte("package strings\nfunc ToUpper(value string) string { return value }\n"), 0o644); err != nil { + t.Fatal(err) + } + + message := runBuildFatal(t, &pb.BuildProgramRequest{ + WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, + }) + if !strings.Contains(message, "import path must be opentaint/") { + t.Fatalf("unexpected validation error: %s", message) + } +} + +func TestBuildProgram_GoModelMustCompile(t *testing.T) { + dir := writeTinyModule(t) + modelDir := writeStringsModel(t, "strings") + if err := os.WriteFile(filepath.Join(modelDir, "strings", "model.go"), []byte("package strings\nfunc ToUpper(value string) string { return missing }\n"), 0o644); err != nil { + t.Fatal(err) + } + + message := runBuildFatal(t, &pb.BuildProgramRequest{ + WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, + }) + if !strings.Contains(message, "Go model does not compile") || !strings.Contains(message, "undefined: missing") { + t.Fatalf("unexpected compilation error: %s", message) + } +} + +func TestBuildProgram_GoModelReplacesReceiverMethodBody(t *testing.T) { + projectDir := t.TempDir() + if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte("module example.com/m\ngo 1.22\n"), 0o644); err != nil { + t.Fatal(err) + } + projectPackageDir := filepath.Join(projectDir, "box") + if err := os.MkdirAll(projectPackageDir, 0o755); err != nil { + t.Fatal(err) + } + projectSource := "package box\ntype Box struct{}\nfunc (b *Box) Value(value string) string { return \"original\" }\n" + if err := os.WriteFile(filepath.Join(projectPackageDir, "box.go"), []byte(projectSource), 0o644); err != nil { + t.Fatal(err) + } + + modelDir := t.TempDir() + if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { + t.Fatal(err) + } + modelPackageDir := filepath.Join(modelDir, "example.com", "m", "box") + if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { + t.Fatal(err) + } + modelSource := "package box\ntype Box struct{}\nfunc (b *Box) Value(value string) string { return value }\n" + if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { + t.Fatal(err) + } + + program := runBuildProgram(t, &pb.BuildProgramRequest{ + WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, + }) + targetPackage := findPkg(program.Packages, "example.com/m/box") + if targetPackage == nil { + t.Fatal("modeled package missing") + } + var target *pb.ProtoFunction + for _, fn := range targetPackage.Functions { + if fn.Name == "Value" { + target = fn + break + } + } + if target == nil || !target.IsMethod || !target.IsPointerReceiver { + t.Fatalf("modeled receiver method missing: %+v", target) + } + for _, body := range program.FunctionBodies { + if body.FunctionId != target.Id || len(body.Blocks) != 1 { + continue + } + instructions := body.Blocks[0].Instructions + ret := instructions[len(instructions)-1].GetReturnInst() + if ret == nil || len(ret.Results) != 1 || ret.Results[0].GetParamIndex() != 1 { + t.Fatalf("modeled method does not return its argument: %+v", ret) + } + return + } + t.Fatal("modeled receiver method body missing") +} + +func TestBuildProgram_GoModelCanBePartialAndAddFieldsAndMethods(t *testing.T) { + projectDir := t.TempDir() + if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte("module example.com/fields\ngo 1.22\n"), 0o644); err != nil { + t.Fatal(err) + } + projectSource := `package fields + +type Box struct { + Original string + Count int +} + +func (b *Box) Put(value string) { b.Original = "original" } +func (b *Box) Get() string { return b.Original } +func (b *Box) Keep(value string) string { return "original" } +` + if err := os.WriteFile(filepath.Join(projectDir, "fields.go"), []byte(projectSource), 0o644); err != nil { + t.Fatal(err) + } + + modelDir := t.TempDir() + if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { + t.Fatal(err) + } + modelPackageDir := filepath.Join(modelDir, "example.com", "fields") + if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { + t.Fatal(err) + } + modelSource := `package fields + +type Box struct { + Shadow string + Original string +} + +func (b *Box) Put(value string) { + b.Shadow = value + b.Original = value +} +func (b *Box) Get() string { return b.Shadow } +func (b *Box) Helper(value string) string { + b.Shadow = value + return b.Shadow +} +` + if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { + t.Fatal(err) + } + + program := runBuildProgram(t, &pb.BuildProgramRequest{ + WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, + }) + targetPackage := findPkg(program.Packages, "example.com/fields") + if targetPackage == nil { + t.Fatal("modeled package missing") + } + + var box *pb.ProtoNamedType + for _, named := range targetPackage.NamedTypes { + if named.Name == "Box" { + box = named + break + } + } + if box == nil { + t.Fatal("modeled Box type missing") + } + if len(box.Fields) != 3 || box.Fields[0].Name != "Original" || box.Fields[0].Index != 0 || + box.Fields[1].Name != "Count" || box.Fields[1].Index != 1 || + box.Fields[2].Name != "Shadow" || box.Fields[2].Index != 2 { + t.Fatalf("model field was not appended to Box: %+v", box.Fields) + } + var underlying *pb.ProtoStructType + for _, typeDefinition := range program.Types { + if typeDefinition.Id == box.UnderlyingTypeId { + underlying = typeDefinition.GetStructType() + break + } + } + if underlying == nil || len(underlying.Fields) != 3 || underlying.Fields[2].Name != "Shadow" { + t.Fatalf("Box underlying type does not contain the model field: %+v", underlying) + } + + functions := make(map[string]*pb.ProtoFunction) + for _, function := range targetPackage.Functions { + if function.ParentFunctionId == 0 { + functions[function.Name] = function + } + } + for _, name := range []string{"Put", "Get", "Keep", "Helper"} { + if functions[name] == nil { + t.Fatalf("function %s is missing", name) + } + } + if !functions["Helper"].IsSynthetic || functions["Helper"].SyntheticKind != "opentaint model support" { + t.Fatalf("model-only method is not synthetic: %+v", functions["Helper"]) + } + + bodies := make(map[int32]*pb.ProtoFunctionBody, len(program.FunctionBodies)) + for _, body := range program.FunctionBodies { + bodies[body.FunctionId] = body + } + for _, name := range []string{"Put", "Get", "Helper"} { + body := bodies[functions[name].Id] + if body == nil { + t.Fatalf("function %s has no model body", name) + } + var sawShadow, sawOriginal bool + for _, block := range body.Blocks { + for _, instruction := range block.Instructions { + if access := instruction.GetFieldAddr(); access != nil && access.FieldName == "Shadow" { + if access.FieldIndex != 2 { + t.Fatalf("function %s uses Shadow index %d, want 2", name, access.FieldIndex) + } + sawShadow = true + } + if access := instruction.GetFieldAddr(); access != nil && access.FieldName == "Original" { + if access.FieldIndex != 0 { + t.Fatalf("function %s uses Original index %d, want 0", name, access.FieldIndex) + } + sawOriginal = true + } + if access := instruction.GetField(); access != nil && access.FieldName == "Shadow" { + if access.FieldIndex != 2 { + t.Fatalf("function %s uses Shadow index %d, want 2", name, access.FieldIndex) + } + sawShadow = true + } + } + } + if !sawShadow { + t.Fatalf("function %s does not use the model-only field", name) + } + if name == "Put" && !sawOriginal { + t.Fatal("Put does not use the existing field") + } + } + + keepBody := bodies[functions["Keep"].Id] + if keepBody == nil { + t.Fatal("unmodeled Keep body is missing") + } + var keptOriginal bool + for _, block := range keepBody.Blocks { + for _, instruction := range block.Instructions { + result := instruction.GetReturnInst() + if result != nil && len(result.Results) == 1 && + result.Results[0].GetConstVal().GetStringValue() == "original" { + keptOriginal = true + } + } + } + if !keptOriginal { + t.Fatal("partial model changed the unmodeled Keep body") + } +} + +func TestBuildProgram_GoModelCanImportTargetProjectModule(t *testing.T) { + projectDir := t.TempDir() + if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte("module example.com/dependency\ngo 1.22\n"), 0o644); err != nil { + t.Fatal(err) + } + projectSource := `package dependency + +type Value struct { Text string } + +func Identity(value Value) Value { return Value{Text: "original"} } +` + if err := os.WriteFile(filepath.Join(projectDir, "dependency.go"), []byte(projectSource), 0o644); err != nil { + t.Fatal(err) + } + + modelDir := t.TempDir() + if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { + t.Fatal(err) + } + modelPackageDir := filepath.Join(modelDir, "example.com", "dependency") + if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { + t.Fatal(err) + } + modelSource := `package dependency + +import target "example.com/dependency" + +func Identity(value target.Value) target.Value { return value } +` + if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { + t.Fatal(err) + } + + program := runBuildProgram(t, &pb.BuildProgramRequest{ + WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, + }) + targetPackage := findPkg(program.Packages, "example.com/dependency") + if targetPackage == nil { + t.Fatal("target dependency package missing") + } + var identity *pb.ProtoFunction + for _, function := range targetPackage.Functions { + if function.Name == "Identity" && function.ParentFunctionId == 0 { + identity = function + break + } + } + if identity == nil { + t.Fatal("target Identity function missing") + } + for _, body := range program.FunctionBodies { + if body.FunctionId != identity.Id { + continue + } + instructions := body.Blocks[0].Instructions + result := instructions[len(instructions)-1].GetReturnInst() + if result == nil || len(result.Results) != 1 || result.Results[0].GetParamIndex() != 0 { + t.Fatalf("target dependency model does not return its parameter: %+v", result) + } + return + } + t.Fatal("target dependency model body missing") +} + +func TestBuildProgram_GoModelCanImportResolvedProjectDependency(t *testing.T) { + dependencyDir := t.TempDir() + if err := os.WriteFile(filepath.Join(dependencyDir, "go.mod"), []byte("module example.com/library\ngo 1.22\n"), 0o644); err != nil { + t.Fatal(err) + } + dependencySource := `package library + +type Value struct { Text string } + +func Identity(value Value) Value { return Value{Text: "original"} } +` + if err := os.WriteFile(filepath.Join(dependencyDir, "library.go"), []byte(dependencySource), 0o644); err != nil { + t.Fatal(err) + } + + projectDir := t.TempDir() + projectGoMod := "module example.com/app\ngo 1.22\nrequire example.com/library v0.0.0\nreplace example.com/library => " + dependencyDir + "\n" + if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte(projectGoMod), 0o644); err != nil { + t.Fatal(err) + } + projectSource := `package main + +import "example.com/library" + +func Use(value library.Value) library.Value { return library.Identity(value) } +` + if err := os.WriteFile(filepath.Join(projectDir, "main.go"), []byte(projectSource), 0o644); err != nil { + t.Fatal(err) + } + + modelDir := t.TempDir() + if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { + t.Fatal(err) + } + modelPackageDir := filepath.Join(modelDir, "example.com", "library") + if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { + t.Fatal(err) + } + modelSource := `package library + +import target "example.com/library" + +func Identity(value target.Value) target.Value { return value } +` + if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { + t.Fatal(err) + } + + program := runBuildProgram(t, &pb.BuildProgramRequest{ + WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, + }) + targetPackage := findPkg(program.Packages, "example.com/library") + if targetPackage == nil || !targetPackage.IsDependency { + t.Fatalf("resolved project dependency is missing: %+v", targetPackage) + } + var identity *pb.ProtoFunction + for _, function := range targetPackage.Functions { + if function.Name == "Identity" && function.ParentFunctionId == 0 { + identity = function + break + } + } + if identity == nil { + t.Fatal("dependency Identity function missing") + } + for _, body := range program.FunctionBodies { + if body.FunctionId != identity.Id { + continue + } + instructions := body.Blocks[0].Instructions + result := instructions[len(instructions)-1].GetReturnInst() + if result == nil || len(result.Results) != 1 || result.Results[0].GetParamIndex() != 0 { + t.Fatalf("resolved dependency model does not return its parameter: %+v", result) + } + return + } + t.Fatal("resolved dependency model body missing") +} + +func TestBuildProgram_GoModelExistingFieldTypeMustMatch(t *testing.T) { + projectDir := t.TempDir() + if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte("module example.com/fieldtype\ngo 1.22\n"), 0o644); err != nil { + t.Fatal(err) + } + projectSource := "package fieldtype\ntype Box struct { Value string }\nfunc (b *Box) Put(value string) {}\n" + if err := os.WriteFile(filepath.Join(projectDir, "fieldtype.go"), []byte(projectSource), 0o644); err != nil { + t.Fatal(err) + } + + modelDir := t.TempDir() + if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { + t.Fatal(err) + } + modelPackageDir := filepath.Join(modelDir, "example.com", "fieldtype") + if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { + t.Fatal(err) + } + modelSource := "package fieldtype\ntype Box struct { Value int }\nfunc (b *Box) Put(value string) { b.Value = len(value) }\n" + if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { + t.Fatal(err) + } + + message := runBuildFatal(t, &pb.BuildProgramRequest{ + WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, + }) + if !strings.Contains(message, "field example.com/fieldtype.Box.Value has a different type") { + t.Fatalf("unexpected field validation error: %s", message) + } +} + +func TestBuildProgram_GoModelSupportsVersionedImportPathWithDifferentPackageName(t *testing.T) { + projectDir := t.TempDir() + if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte("module github.com/acme/widget/v2\ngo 1.22\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(projectDir, "widget.go"), []byte("package widget\nfunc Identity(value string) string { return \"original\" }\n"), 0o644); err != nil { + t.Fatal(err) + } + + modelDir := t.TempDir() + if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { + t.Fatal(err) + } + modelPackageDir := filepath.Join(modelDir, "github.com", "acme", "widget", "v2") + if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte("package widget\nfunc Identity(value string) string { return value }\n"), 0o644); err != nil { + t.Fatal(err) + } + + program := runBuildProgram(t, &pb.BuildProgramRequest{ + WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, + }) + targetPackage := findPkg(program.Packages, "github.com/acme/widget/v2") + if targetPackage == nil || targetPackage.Name != "widget" { + t.Fatalf("versioned modeled package missing: %+v", targetPackage) + } + target := targetPackage.Functions[0] + for _, body := range program.FunctionBodies { + if body.FunctionId != target.Id { + continue + } + ret := body.Blocks[0].Instructions[len(body.Blocks[0].Instructions)-1].GetReturnInst() + if ret == nil || len(ret.Results) != 1 || ret.Results[0].GetParamIndex() != 0 { + t.Fatalf("versioned package model body not applied: %+v", ret) + } + return + } + t.Fatal("versioned package model body missing") +} + +func TestBuildProgram_GoModelAddsSyntheticHelpersUsedByModelBody(t *testing.T) { + dir := writeTinyModule(t) + modelDir := t.TempDir() + if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { + t.Fatal(err) + } + modelPackageDir := filepath.Join(modelDir, "strings") + if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { + t.Fatal(err) + } + modelSource := "package strings\nfunc identity(value string) string { return value }\nfunc ToUpper(value string) string { return identity(value) }\n" + if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { + t.Fatal(err) + } + + program := runBuildProgram(t, &pb.BuildProgramRequest{ + WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, + }) + stringsPackage := findPkg(program.Packages, "strings") + var target, helper *pb.ProtoFunction + for _, fn := range stringsPackage.Functions { + switch fn.Name { + case "ToUpper": + target = fn + case "identity": + helper = fn + } + } + if target == nil || helper == nil { + t.Fatalf("target or helper missing: target=%+v helper=%+v", target, helper) + } + if !helper.IsSynthetic || helper.SyntheticKind != "opentaint model support" { + t.Fatalf("model helper is not synthetic: %+v", helper) + } + + var targetCallsHelper, helperHasBody bool + for _, body := range program.FunctionBodies { + if body.FunctionId == helper.Id { + helperHasBody = true + } + if body.FunctionId != target.Id { + continue + } + for _, block := range body.Blocks { + for _, instruction := range block.Instructions { + call := instruction.GetCall() + if call != nil && call.GetCall().GetFunction().GetFunctionId() == helper.Id { + targetCallsHelper = true + } + } + } + } + if !targetCallsHelper || !helperHasBody { + t.Fatalf("model helper reference was not preserved: called=%t body=%t", targetCallsHelper, helperHasBody) + } +} + +func TestBuildProgram_GoModelOwnsItsAnonymousFunctions(t *testing.T) { + projectDir := t.TempDir() + if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte("module example.com/closures\ngo 1.22\n"), 0o644); err != nil { + t.Fatal(err) + } + projectSource := "package closures\nfunc Apply(value string) string { nested := func() string { return \"original\" }; return nested() }\n" + if err := os.WriteFile(filepath.Join(projectDir, "closures.go"), []byte(projectSource), 0o644); err != nil { + t.Fatal(err) + } + + modelDir := t.TempDir() + if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { + t.Fatal(err) + } + modelPackageDir := filepath.Join(modelDir, "example.com", "closures") + if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { + t.Fatal(err) + } + modelSource := "package closures\nfunc Apply(value string) string { nested := func(input string) string { return input }; return nested(value) }\n" + if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { + t.Fatal(err) + } + + program := runBuildProgram(t, &pb.BuildProgramRequest{ + WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, + }) + targetPackage := findPkg(program.Packages, "example.com/closures") + var target *pb.ProtoFunction + functions := functionsByID(targetPackage.Functions) + for _, fn := range targetPackage.Functions { + if fn.Name == "Apply" && fn.ParentFunctionId == 0 { + target = fn + break + } + } + if target == nil || len(target.AnonFunctionIds) != 1 { + t.Fatalf("modeled parent does not own its closure: %+v", target) + } + closure := functions[target.AnonFunctionIds[0]] + if closure == nil || closure.ParentFunctionId != target.Id || len(closure.Params) != 1 || !closure.IsSynthetic { + t.Fatalf("model closure metadata not preserved: %+v", closure) + } + for _, body := range program.FunctionBodies { + if body.FunctionId == closure.Id { + return + } + } + t.Fatal("model closure body missing") +} diff --git a/core/opentaint-ir/go/go-ssa-server/builder.go b/core/opentaint-ir/go/go-ssa-server/builder.go index 41a3a30259..34ee94bd4e 100644 --- a/core/opentaint-ir/go/go-ssa-server/builder.go +++ b/core/opentaint-ir/go/go-ssa-server/builder.go @@ -15,17 +15,35 @@ import ( ) func buildSSA(req *pb.BuildProgramRequest) (*ssa.Program, []*ssa.Package, map[string]*packages.Package, error) { + return buildSSAWithPackageErrors(req, false) +} + +func buildSSAWithPackageErrors(req *pb.BuildProgramRequest, failOnPackageErrors bool) (*ssa.Program, []*ssa.Package, map[string]*packages.Package, error) { + return buildSSAWithPackageErrorsAndEnv(req, failOnPackageErrors, nil) +} + +func buildSSAWithPackageErrorsAndEnv( + req *pb.BuildProgramRequest, + failOnPackageErrors bool, + extraEnv []string, +) (*ssa.Program, []*ssa.Package, map[string]*packages.Package, error) { cfg := &packages.Config{ Mode: packages.NeedFiles | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedImports | packages.NeedDeps | packages.NeedName | packages.NeedModule, Dir: req.WorkingDir, } + if len(extraEnv) > 0 { + cfg.Env = append(os.Environ(), extraEnv...) + } if len(req.BuildTags) > 0 { cfg.BuildFlags = []string{"-tags=" + strings.Join(req.BuildTags, ",")} } if req.Gopath != "" { - cfg.Env = append(os.Environ(), "GOPATH="+req.Gopath) + if cfg.Env == nil { + cfg.Env = os.Environ() + } + cfg.Env = append(cfg.Env, "GOPATH="+req.Gopath) } if req.Goroot != "" { if cfg.Env == nil { @@ -55,6 +73,19 @@ func buildSSA(req *pb.BuildProgramRequest) (*ssa.Program, []*ssa.Package, map[st log.Printf("WARN: package %s: %v", p.PkgPath, e) } } + if failOnPackageErrors { + var loadErrors []string + packages.Visit(loaded, func(p *packages.Package) bool { + for _, e := range p.Errors { + loadErrors = append(loadErrors, fmt.Sprintf("%s: %v", p.PkgPath, e)) + } + return true + }, nil) + if len(loadErrors) > 0 { + sort.Strings(loadErrors) + return nil, nil, nil, fmt.Errorf("Go model does not compile:\n%s", strings.Join(loadErrors, "\n")) + } + } mode := ssa.BuilderMode(0) if req.InstantiateGenerics { @@ -77,7 +108,11 @@ func buildSSA(req *pb.BuildProgramRequest) (*ssa.Program, []*ssa.Package, map[st return prog, all, info, nil } -func classifyPackage(p *packages.Package, projectModulePaths map[string]bool) (isStdlib bool, isDependency bool) { +func classifyPackage( + p *packages.Package, + projectModulePaths map[string]bool, + onlyListedProjectModules bool, +) (isStdlib bool, isDependency bool) { if p == nil { return false, true } @@ -85,6 +120,7 @@ func classifyPackage(p *packages.Package, projectModulePaths map[string]bool) (i if isStdlib { return true, false } - isProject := p.Module != nil && (p.Module.Main || projectModulePaths[p.Module.Path]) + isProject := p.Module != nil && (projectModulePaths[p.Module.Path] || + (!onlyListedProjectModules && p.Module.Main)) return false, !isProject } diff --git a/core/opentaint-ir/go/go-ssa-server/model_merge.go b/core/opentaint-ir/go/go-ssa-server/model_merge.go new file mode 100644 index 0000000000..26d0bfca5f --- /dev/null +++ b/core/opentaint-ir/go/go-ssa-server/model_merge.go @@ -0,0 +1,1053 @@ +package server + +import ( + "fmt" + "sort" + "strconv" + "strings" + + pb "github.com/opentaint/go-ir/go-ssa-server/proto/goir" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// A model package uses opentaint/ as its import path. +// Remove opentaint/ to get the exact target import path. This rule works for +// standard and module packages such as opentaint/strings, opentaint/net/http, +// and opentaint/github.com/acme/lib. +const goModelPrefix = "opentaint/" + +type modelMergeState struct { + modeledPackages map[string]string + modeledFunctions map[int32]string +} + +func newModelMergeState() *modelMergeState { + return &modelMergeState{ + modeledPackages: make(map[string]string), + modeledFunctions: make(map[int32]string), + } +} + +type modelPathNormalizer struct { + replacements map[string]string + ordered []string + targets map[string]bool +} + +func modelNormalizer(program *pb.ProtoProgram) (*modelPathNormalizer, error) { + n := &modelPathNormalizer{ + replacements: make(map[string]string), + targets: make(map[string]bool), + } + for _, pkg := range program.Packages { + if pkg.IsDependency || pkg.IsStdlib || pkg.ImportPath == universePackagePath { + continue + } + if !strings.HasPrefix(pkg.ImportPath, goModelPrefix) { + return nil, fmt.Errorf( + "Go model package %q is not model-addressed; its import path must be %s", + pkg.ImportPath, goModelPrefix, + ) + } + target := strings.TrimPrefix(pkg.ImportPath, goModelPrefix) + if target == "" { + return nil, fmt.Errorf("Go model package %q has an empty target package", pkg.ImportPath) + } + n.replacements[pkg.ImportPath] = target + n.targets[target] = true + n.ordered = append(n.ordered, pkg.ImportPath) + } + if len(n.replacements) == 0 { + return nil, fmt.Errorf( + "Go model contains no model-addressed packages; expected at least one package under %s", + goModelPrefix, + ) + } + sort.Slice(n.ordered, func(i, j int) bool { return len(n.ordered[i]) > len(n.ordered[j]) }) + return n, nil +} + +func (n *modelPathNormalizer) path(value string) string { + if replacement, ok := n.replacements[value]; ok { + return replacement + } + return value +} + +func (n *modelPathNormalizer) symbol(value string) string { + for _, markerPath := range n.ordered { + value = strings.ReplaceAll(value, markerPath, n.replacements[markerPath]) + } + return value +} + +type protoIndex struct { + program *pb.ProtoProgram + norm *modelPathNormalizer + + packages map[int32]*pb.ProtoPackage + types map[int32]*pb.ProtoTypeDefinition + named map[int32]*pb.ProtoNamedType + funcs map[int32]*pb.ProtoFunction + globals map[int32]*pb.ProtoGlobal + consts map[int32]*pb.ProtoConst + + packageKey map[int32]string + namedKey map[int32]string + typeMemo map[int32]string + typeActive map[int32]bool +} + +func indexProto(program *pb.ProtoProgram, norm *modelPathNormalizer) *protoIndex { + idx := &protoIndex{ + program: program, + norm: norm, + packages: make(map[int32]*pb.ProtoPackage), + types: make(map[int32]*pb.ProtoTypeDefinition), + named: make(map[int32]*pb.ProtoNamedType), + funcs: make(map[int32]*pb.ProtoFunction), + globals: make(map[int32]*pb.ProtoGlobal), + consts: make(map[int32]*pb.ProtoConst), + packageKey: make(map[int32]string), + namedKey: make(map[int32]string), + typeMemo: make(map[int32]string), + typeActive: make(map[int32]bool), + } + for _, td := range program.Types { + idx.types[td.Id] = td + } + for _, pkg := range program.Packages { + idx.packages[pkg.Id] = pkg + pkgPath := pkg.ImportPath + if norm != nil { + pkgPath = norm.path(pkgPath) + } + idx.packageKey[pkg.Id] = pkgPath + for _, named := range pkg.NamedTypes { + idx.named[named.Id] = named + idx.namedKey[named.Id] = pkgPath + "." + named.Name + } + for _, fn := range pkg.Functions { + idx.funcs[fn.Id] = fn + } + for _, global := range pkg.Globals { + idx.globals[global.Id] = global + } + for _, c := range pkg.Constants { + idx.consts[c.Id] = c + } + } + return idx +} + +func (i *protoIndex) symbol(value string) string { + if i.norm == nil { + return value + } + return i.norm.symbol(value) +} + +func (i *protoIndex) typeKey(id int32) string { + if id == 0 { + return "void" + } + if key, ok := i.typeMemo[id]; ok { + return key + } + if i.typeActive[id] { + // Named references break every ordinary recursive Go type cycle. This + // fallback is for malformed or future wire types and remains stable + // across independently loaded programs. + return "" + } + td := i.types[id] + if td == nil { + return "" + } + i.typeActive[id] = true + defer delete(i.typeActive, id) + + var key string + switch { + case td.GetBasic() != nil: + key = "basic:" + strconv.Itoa(int(td.GetBasic().Kind)) + case td.GetPointer() != nil: + key = "pointer:" + i.typeKey(td.GetPointer().ElemTypeId) + case td.GetArray() != nil: + key = "array:" + strconv.FormatInt(td.GetArray().Length, 10) + ":" + i.typeKey(td.GetArray().ElemTypeId) + case td.GetSlice() != nil: + key = "slice:" + i.typeKey(td.GetSlice().ElemTypeId) + case td.GetMapType() != nil: + key = "map:" + i.typeKey(td.GetMapType().KeyTypeId) + ":" + i.typeKey(td.GetMapType().ValueTypeId) + case td.GetChanType() != nil: + key = "chan:" + strconv.Itoa(int(td.GetChanType().Direction)) + ":" + i.typeKey(td.GetChanType().ElemTypeId) + case td.GetStructType() != nil: + var b strings.Builder + b.WriteString("struct{") + for _, field := range td.GetStructType().Fields { + fmt.Fprintf(&b, "%s:%s:%t:%t:%s;", field.Name, i.typeKey(field.TypeId), field.Embedded, field.Exported, field.Tag) + } + b.WriteByte('}') + key = b.String() + case td.GetInterfaceType() != nil: + var b strings.Builder + b.WriteString("interface{") + for _, method := range td.GetInterfaceType().Methods { + fmt.Fprintf(&b, "%s:%s;", method.Name, i.typeKey(method.SignatureTypeId)) + } + for _, embedded := range td.GetInterfaceType().EmbedTypeIds { + b.WriteString("embed:") + b.WriteString(i.typeKey(embedded)) + b.WriteByte(';') + } + b.WriteByte('}') + key = b.String() + case td.GetFuncType() != nil: + fn := td.GetFuncType() + var b strings.Builder + b.WriteString("func(") + for _, param := range fn.ParamTypeIds { + b.WriteString(i.typeKey(param)) + b.WriteByte(',') + } + b.WriteString(")->(") + for _, result := range fn.ResultTypeIds { + b.WriteString(i.typeKey(result)) + b.WriteByte(',') + } + fmt.Fprintf(&b, "):variadic=%t:recv=%s", fn.Variadic, i.typeKey(fn.RecvTypeId)) + key = b.String() + case td.GetNamedRef() != nil: + ref := td.GetNamedRef() + var b strings.Builder + b.WriteString("named:") + b.WriteString(i.namedKey[ref.NamedTypeId]) + b.WriteByte('[') + for _, arg := range ref.TypeArgIds { + b.WriteString(i.typeKey(arg)) + b.WriteByte(',') + } + b.WriteByte(']') + key = b.String() + case td.GetTypeParam() != nil: + param := td.GetTypeParam() + key = fmt.Sprintf("typeparam:%s:%d:%s", param.Name, param.Index, i.typeKey(param.ConstraintTypeId)) + case td.GetTuple() != nil: + var b strings.Builder + b.WriteString("tuple(") + for _, elem := range td.GetTuple().ElementTypeIds { + b.WriteString(i.typeKey(elem)) + b.WriteByte(',') + } + b.WriteByte(')') + key = b.String() + case td.GetUnsafePointer() != nil: + key = "unsafe.Pointer" + default: + key = "" + } + i.typeMemo[id] = key + return key +} + +type idRemap struct { + types map[int32]int32 + packages map[int32]int32 + functions map[int32]int32 + named map[int32]int32 + globals map[int32]int32 + consts map[int32]int32 +} + +func newIDRemap() *idRemap { + return &idRemap{ + types: make(map[int32]int32), + packages: make(map[int32]int32), + functions: make(map[int32]int32), + named: make(map[int32]int32), + globals: make(map[int32]int32), + consts: make(map[int32]int32), + } +} + +type idCounters struct { + typeID, packageID, functionID, namedID, globalID, constID int32 +} + +func countersFor(program *pb.ProtoProgram) idCounters { + var result idCounters + for _, td := range program.Types { + result.typeID = max(result.typeID, td.Id) + } + for _, pkg := range program.Packages { + result.packageID = max(result.packageID, pkg.Id) + for _, named := range pkg.NamedTypes { + result.namedID = max(result.namedID, named.Id) + } + for _, fn := range pkg.Functions { + result.functionID = max(result.functionID, fn.Id) + } + for _, global := range pkg.Globals { + result.globalID = max(result.globalID, global.Id) + } + for _, c := range pkg.Constants { + result.constID = max(result.constID, c.Id) + } + } + return result +} + +func reversePackageKeys(i *protoIndex) map[string]int32 { + result := make(map[string]int32, len(i.packageKey)) + for id, key := range i.packageKey { + result[key] = id + } + return result +} + +func typeKeysByID(i *protoIndex) map[int32]string { + result := make(map[int32]string, len(i.types)) + for id := range i.types { + result[id] = i.typeKey(id) + } + return result +} + +func functionKeysByID(i *protoIndex) map[int32]string { + result := make(map[int32]string, len(i.funcs)) + for id, fn := range i.funcs { + result[id] = i.symbol(fn.FullName) + } + return result +} + +func globalKeysByID(i *protoIndex) map[int32]string { + result := make(map[int32]string, len(i.globals)) + for id, global := range i.globals { + result[id] = i.symbol(global.FullName) + } + return result +} + +func constKeysByID(i *protoIndex) map[int32]string { + result := make(map[int32]string, len(i.consts)) + for id, c := range i.consts { + result[id] = i.symbol(c.FullName) + } + return result +} + +func reverseNamedKeys(i *protoIndex) map[string]int32 { + result := make(map[string]int32, len(i.namedKey)) + for id, key := range i.namedKey { + result[key] = id + } + return result +} + +func reverseTypeKeys(i *protoIndex) map[string]int32 { + result := make(map[string]int32, len(i.types)) + for id := range i.types { + result[i.typeKey(id)] = id + } + return result +} + +func reverseFunctionKeys(i *protoIndex) map[string]int32 { + result := make(map[string]int32, len(i.funcs)) + for id, fn := range i.funcs { + result[i.symbol(fn.FullName)] = id + } + return result +} + +func reverseGlobalKeys(i *protoIndex) map[string]int32 { + result := make(map[string]int32, len(i.globals)) + for id, global := range i.globals { + result[i.symbol(global.FullName)] = id + } + return result +} + +func reverseConstKeys(i *protoIndex) map[string]int32 { + result := make(map[string]int32, len(i.consts)) + for id, c := range i.consts { + result[i.symbol(c.FullName)] = id + } + return result +} + +func assignIDs[K comparable](source map[int32]K, existing map[K]int32, next *int32) map[int32]int32 { + result := make(map[int32]int32, len(source)) + oldIDs := make([]int, 0, len(source)) + for oldID := range source { + oldIDs = append(oldIDs, int(oldID)) + } + sort.Ints(oldIDs) + for _, rawOldID := range oldIDs { + oldID := int32(rawOldID) + key := source[oldID] + if id, ok := existing[key]; ok { + result[oldID] = id + continue + } + *next = *next + 1 + result[oldID] = *next + existing[key] = *next + } + return result +} + +func remapValue(old int32, values map[int32]int32) (int32, error) { + if old == 0 { + return 0, nil + } + value, ok := values[old] + if !ok { + return 0, fmt.Errorf("wire id %d has no model remapping", old) + } + return value, nil +} + +func remapClass(message protoreflect.Message, field protoreflect.FieldDescriptor) mapName { + name := string(field.Name()) + if name == "id" { + switch message.Descriptor().Name() { + case "ProtoTypeDefinition": + return typeMap + case "ProtoPackage": + return packageMap + case "ProtoNamedType": + return namedMap + case "ProtoFunction": + return functionMap + case "ProtoGlobal": + return globalMap + case "ProtoConst": + return constMap + } + } + if name == "named_type_id" || name == "embedded_interface_ids" { + return namedMap + } + if name == "package_id" || name == "import_ids" { + return packageMap + } + if name == "fn_id" || name == "method_ids" || name == "pointer_method_ids" || + strings.Contains(name, "function_id") { + return functionMap + } + if name == "global_id" { + return globalMap + } + if strings.HasSuffix(name, "type_id") || strings.HasSuffix(name, "type_ids") { + return typeMap + } + return noMap +} + +type mapName uint8 + +const ( + noMap mapName = iota + typeMap + packageMap + functionMap + namedMap + globalMap + constMap +) + +func (r *idRemap) values(name mapName) map[int32]int32 { + switch name { + case typeMap: + return r.types + case packageMap: + return r.packages + case functionMap: + return r.functions + case namedMap: + return r.named + case globalMap: + return r.globals + case constMap: + return r.consts + default: + return nil + } +} + +func remapMessageIDs(message protoreflect.Message, remap *idRemap) error { + var result error + message.Range(func(field protoreflect.FieldDescriptor, value protoreflect.Value) bool { + if field.IsList() { + list := value.List() + if field.Kind() == protoreflect.MessageKind { + for idx := 0; idx < list.Len(); idx++ { + if err := remapMessageIDs(list.Get(idx).Message(), remap); err != nil { + result = err + return false + } + } + return true + } + class := remapClass(message, field) + if class == noMap { + return true + } + for idx := 0; idx < list.Len(); idx++ { + mapped, err := remapValue(int32(list.Get(idx).Int()), remap.values(class)) + if err != nil { + result = fmt.Errorf("remapping %s.%s: %w", message.Descriptor().Name(), field.Name(), err) + return false + } + list.Set(idx, protoreflect.ValueOfInt32(mapped)) + } + return true + } + if field.Kind() == protoreflect.MessageKind { + if err := remapMessageIDs(value.Message(), remap); err != nil { + result = err + return false + } + return true + } + class := remapClass(message, field) + if class == noMap { + return true + } + mapped, err := remapValue(int32(value.Int()), remap.values(class)) + if err != nil { + result = fmt.Errorf("remapping %s.%s: %w", message.Descriptor().Name(), field.Name(), err) + return false + } + message.Set(field, protoreflect.ValueOfInt32(mapped)) + return true + }) + return result +} + +type modelStructFieldPlan struct { + targetNamedID int32 + indexByModel map[int32]int32 + extraFields []*pb.ProtoFieldDecl +} + +func planModelStructFields( + baseIndex, modelIndex *protoIndex, + norm *modelPathNormalizer, +) (map[int32]*modelStructFieldPlan, error) { + plans := make(map[int32]*modelStructFieldPlan) + baseNamed := reverseNamedKeys(baseIndex) + + for _, pkg := range modelIndex.program.Packages { + if _, owned := norm.replacements[pkg.ImportPath]; !owned { + continue + } + for _, modelNamed := range pkg.NamedTypes { + targetID, exists := baseNamed[modelIndex.namedKey[modelNamed.Id]] + if !exists { + continue + } + targetNamed := baseIndex.named[targetID] + if modelNamed.Kind != pb.ProtoNamedTypeKind_NAMED_TYPE_STRUCT { + continue + } + if targetNamed.Kind != pb.ProtoNamedTypeKind_NAMED_TYPE_STRUCT { + return nil, fmt.Errorf( + "go model type %s is a struct, but its target is not a struct", + modelIndex.namedKey[modelNamed.Id], + ) + } + + targetFields := make(map[string]*pb.ProtoFieldDecl, len(targetNamed.Fields)) + var nextIndex int32 + for _, field := range targetNamed.Fields { + targetFields[field.Name] = field + nextIndex = max(nextIndex, field.Index+1) + } + + plan := &modelStructFieldPlan{ + targetNamedID: targetID, + indexByModel: make(map[int32]int32, len(modelNamed.Fields)), + } + for _, modelField := range modelNamed.Fields { + modelIndexValue := modelField.Index + if targetField := targetFields[modelField.Name]; targetField != nil { + if modelIndex.typeKey(modelField.TypeId) != baseIndex.typeKey(targetField.TypeId) { + return nil, fmt.Errorf( + "go model field %s.%s has a different type from its target", + modelIndex.namedKey[modelNamed.Id], modelField.Name, + ) + } + plan.indexByModel[modelIndexValue] = targetField.Index + modelField.Index = targetField.Index + continue + } + + plan.indexByModel[modelIndexValue] = nextIndex + modelField.Index = nextIndex + plan.extraFields = append(plan.extraFields, modelField) + nextIndex++ + } + + if underlying := modelIndex.types[modelNamed.UnderlyingTypeId].GetStructType(); underlying != nil { + for _, field := range underlying.Fields { + if targetIndex, exists := plan.indexByModel[field.Index]; exists { + field.Index = targetIndex + } + } + } + plans[modelNamed.Id] = plan + } + } + + return plans, nil +} + +func modelNamedTypeForFieldAccess( + index *protoIndex, + underlyingToNamed map[int32]int32, + typeID int32, + active map[int32]bool, +) int32 { + if typeID == 0 || active[typeID] { + return 0 + } + active[typeID] = true + defer delete(active, typeID) + + td := index.types[typeID] + if td == nil { + return 0 + } + if pointer := td.GetPointer(); pointer != nil { + return modelNamedTypeForFieldAccess(index, underlyingToNamed, pointer.ElemTypeId, active) + } + if named := td.GetNamedRef(); named != nil { + return named.NamedTypeId + } + if td.GetStructType() != nil { + return underlyingToNamed[typeID] + } + return 0 +} + +func remapModelStructFieldAccesses( + model *pb.ProtoProgram, + modelIndex *protoIndex, + plans map[int32]*modelStructFieldPlan, +) error { + underlyingToNamed := make(map[int32]int32, len(modelIndex.named)) + for namedID, named := range modelIndex.named { + underlyingToNamed[named.UnderlyingTypeId] = namedID + } + + remapAccess := func(typeID, fieldIndex int32, fieldName string) (int32, error) { + namedID := modelNamedTypeForFieldAccess( + modelIndex, underlyingToNamed, typeID, make(map[int32]bool), + ) + plan := plans[namedID] + if plan == nil { + return fieldIndex, nil + } + targetIndex, exists := plan.indexByModel[fieldIndex] + if !exists { + return 0, fmt.Errorf( + "go model field access %s[%d] has no target field mapping", + fieldName, fieldIndex, + ) + } + return targetIndex, nil + } + + for _, body := range model.FunctionBodies { + for _, block := range body.Blocks { + for _, instruction := range block.Instructions { + if access := instruction.GetFieldAddr(); access != nil { + fieldIndex, err := remapAccess(access.X.TypeId, access.FieldIndex, access.FieldName) + if err != nil { + return err + } + access.FieldIndex = fieldIndex + } + if access := instruction.GetField(); access != nil { + fieldIndex, err := remapAccess(access.X.TypeId, access.FieldIndex, access.FieldName) + if err != nil { + return err + } + access.FieldIndex = fieldIndex + } + } + } + } + return nil +} + +func mergeModelStructFields( + base *pb.ProtoProgram, + baseIndex *protoIndex, + plans map[int32]*modelStructFieldPlan, + counters *idCounters, +) error { + modelNamedIDs := make([]int, 0, len(plans)) + for modelNamedID := range plans { + modelNamedIDs = append(modelNamedIDs, int(modelNamedID)) + } + sort.Ints(modelNamedIDs) + + for _, rawModelNamedID := range modelNamedIDs { + plan := plans[int32(rawModelNamedID)] + if len(plan.extraFields) == 0 { + continue + } + targetNamed := baseIndex.named[plan.targetNamedID] + if targetNamed == nil { + return fmt.Errorf("go model target named type %d is missing", plan.targetNamedID) + } + underlying := baseIndex.types[targetNamed.UnderlyingTypeId].GetStructType() + if underlying == nil { + return fmt.Errorf("go model target type %s has no struct body", targetNamed.FullName) + } + + combinedFields := make([]*pb.ProtoStructField, 0, len(underlying.Fields)+len(plan.extraFields)) + for _, field := range underlying.Fields { + combinedFields = append(combinedFields, proto.Clone(field).(*pb.ProtoStructField)) + } + for _, field := range plan.extraFields { + targetNamed.Fields = append(targetNamed.Fields, proto.Clone(field).(*pb.ProtoFieldDecl)) + combinedFields = append(combinedFields, &pb.ProtoStructField{ + Name: field.Name, + TypeId: field.TypeId, + Index: field.Index, + Embedded: field.Embedded, + Exported: field.Exported, + Tag: field.Tag, + }) + } + + counters.typeID++ + combined := &pb.ProtoTypeDefinition{ + Id: counters.typeID, + Type: &pb.ProtoTypeDefinition_StructType{StructType: &pb.ProtoStructType{ + Fields: combinedFields, + }}, + } + base.Types = append(base.Types, combined) + baseIndex.types[combined.Id] = combined + targetNamed.UnderlyingTypeId = combined.Id + } + return nil +} + +func mergeGoModel(base *pb.ProtoProgram, rawModel *pb.ProtoProgram, state *modelMergeState, source string) error { + norm, err := modelNormalizer(rawModel) + if err != nil { + return fmt.Errorf("invalid Go model %s: %w", source, err) + } + for target := range norm.targets { + if previous, exists := state.modeledPackages[target]; exists { + return fmt.Errorf("Go package %q is modeled more than once (%s and %s)", target, previous, source) + } + state.modeledPackages[target] = source + } + + model := proto.Clone(rawModel).(*pb.ProtoProgram) + baseIndex := indexProto(base, nil) + modelIndex := indexProto(model, norm) + fieldPlans, err := planModelStructFields(baseIndex, modelIndex, norm) + if err != nil { + return fmt.Errorf("invalid Go model %s: %w", source, err) + } + if err := remapModelStructFieldAccesses(model, modelIndex, fieldPlans); err != nil { + return fmt.Errorf("invalid Go model %s: %w", source, err) + } + modelOwnedOldPackages := make(map[int32]bool) + modelInitOldFunctions := make(map[int32]bool) + for _, pkg := range model.Packages { + if _, ok := norm.replacements[pkg.ImportPath]; !ok { + continue + } + modelOwnedOldPackages[pkg.Id] = true + for _, fn := range pkg.Functions { + if fn.Name == "init" || strings.HasPrefix(fn.Name, "init#") { + modelInitOldFunctions[fn.Id] = true + } + } + } + // Closures declared inside an initializer are initializer implementation + // details too. Exclude the whole descendant tree, not just the synthetic + // package initializer and the compiler-numbered init#N roots. + for changed := true; changed; { + changed = false + for _, pkg := range model.Packages { + if !modelOwnedOldPackages[pkg.Id] { + continue + } + for _, fn := range pkg.Functions { + if !modelInitOldFunctions[fn.Id] && modelInitOldFunctions[fn.ParentFunctionId] { + modelInitOldFunctions[fn.Id] = true + changed = true + } + } + } + } + basePackages := reversePackageKeys(baseIndex) + for _, pkg := range model.Packages { + target, owned := norm.replacements[pkg.ImportPath] + if !owned { + continue + } + basePackageID, targetExists := basePackages[target] + if targetExists && pkg.Name != baseIndex.packages[basePackageID].Name { + return fmt.Errorf( + "Go model package %q has package name %q, want %q (the modeled package name)", + pkg.ImportPath, pkg.Name, baseIndex.packages[basePackageID].Name, + ) + } + } + counters := countersFor(base) + remap := newIDRemap() + + modelPackageKeys := modelIndex.packageKey + modelNamedKeys := modelIndex.namedKey + modelTypeKeys := typeKeysByID(modelIndex) + modelFunctionKeys := functionKeysByID(modelIndex) + // Anonymous functions are implementation details of the modeled body, not + // declarations to match against similarly numbered closures in the target + // body. Give them fresh identities even when their generated full names + // happen to be equal. + for id, fn := range modelIndex.funcs { + if fn.ParentFunctionId != 0 { + modelFunctionKeys[id] += fmt.Sprintf("#opentaint-model-anonymous:%d", id) + } + } + modelGlobalKeys := globalKeysByID(modelIndex) + modelConstKeys := constKeysByID(modelIndex) + + remap.packages = assignIDs(modelPackageKeys, reversePackageKeys(baseIndex), &counters.packageID) + remap.named = assignIDs(modelNamedKeys, reverseNamedKeys(baseIndex), &counters.namedID) + remap.types = assignIDs(modelTypeKeys, reverseTypeKeys(baseIndex), &counters.typeID) + remap.functions = assignIDs(modelFunctionKeys, reverseFunctionKeys(baseIndex), &counters.functionID) + remap.globals = assignIDs(modelGlobalKeys, reverseGlobalKeys(baseIndex), &counters.globalID) + remap.consts = assignIDs(modelConstKeys, reverseConstKeys(baseIndex), &counters.constID) + + // A body is attached to the target declaration, so a same-named model + // must have exactly the target signature. Go has no overloads that could + // otherwise disambiguate the mismatch. + baseFunctions := reverseFunctionKeys(baseIndex) + for modelID, key := range modelFunctionKeys { + baseID, exists := baseFunctions[key] + if !exists { + continue + } + modelFn := modelIndex.funcs[modelID] + if modelFn == nil || modelFn.ParentFunctionId != 0 || modelInitOldFunctions[modelID] || + !modelOwnedPackage(modelIndex, norm, modelFn.PackageId) { + continue + } + baseFn := baseIndex.funcs[baseID] + if modelIndex.typeKey(modelFn.SignatureTypeId) != baseIndex.typeKey(baseFn.SignatureTypeId) || + modelFn.IsMethod != baseFn.IsMethod || modelFn.IsPointerReceiver != baseFn.IsPointerReceiver { + return fmt.Errorf("Go model function %s has a different signature from its target", key) + } + } + + for _, pkg := range model.Packages { + if !modelOwnedOldPackages[pkg.Id] { + continue + } + pkg.InitFunctionId = 0 + pkg.ImportPath = norm.path(pkg.ImportPath) + pkg.IsDependency = true + pkg.IsStdlib = false + } + for _, pkg := range model.Packages { + for _, named := range pkg.NamedTypes { + named.FullName = norm.symbol(named.FullName) + } + for _, fn := range pkg.Functions { + fn.FullName = norm.symbol(fn.FullName) + } + for _, global := range pkg.Globals { + global.FullName = norm.symbol(global.FullName) + } + for _, c := range pkg.Constants { + c.FullName = norm.symbol(c.FullName) + } + } + + modelOwnedPackages := make(map[int32]bool, len(modelOwnedOldPackages)) + for id := range modelOwnedOldPackages { + modelOwnedPackages[remap.packages[id]] = true + } + modelInitFunctions := make(map[int32]bool, len(modelInitOldFunctions)) + for id := range modelInitOldFunctions { + modelInitFunctions[remap.functions[id]] = true + } + + if err := remapMessageIDs(model.ProtoReflect(), remap); err != nil { + return fmt.Errorf("merging Go model %s: %w", source, err) + } + + baseIndex = indexProto(base, nil) + if err := mergeModelStructFields(base, baseIndex, fieldPlans, &counters); err != nil { + return fmt.Errorf("merging Go model %s: %w", source, err) + } + for _, td := range model.Types { + if baseIndex.types[td.Id] == nil { + base.Types = append(base.Types, td) + baseIndex.types[td.Id] = td + } + } + + for _, modelPkg := range model.Packages { + basePkg := baseIndex.packages[modelPkg.Id] + if basePkg == nil { + if modelOwnedPackages[modelPkg.Id] { + functions := modelPkg.Functions[:0] + for _, fn := range modelPkg.Functions { + if modelInitFunctions[fn.Id] { + continue + } + fn.IsSynthetic = true + fn.SyntheticKind = "opentaint model support" + functions = append(functions, fn) + } + modelPkg.Functions = functions + } + base.Packages = append(base.Packages, modelPkg) + baseIndex.packages[modelPkg.Id] = modelPkg + basePkg = modelPkg + continue + } + + basePkg.ImportIds = appendUniqueIDs(basePkg.ImportIds, modelPkg.ImportIds...) + existingNamed := namedByID(basePkg.NamedTypes) + for _, named := range modelPkg.NamedTypes { + if current := existingNamed[named.Id]; current != nil { + current.MethodIds = appendUniqueIDs(current.MethodIds, named.MethodIds...) + current.PointerMethodIds = appendUniqueIDs(current.PointerMethodIds, named.PointerMethodIds...) + continue + } + basePkg.NamedTypes = append(basePkg.NamedTypes, named) + existingNamed[named.Id] = named + } + + existingFunctions := functionsByID(basePkg.Functions) + for _, fn := range modelPkg.Functions { + if modelInitFunctions[fn.Id] { + continue + } + if current := existingFunctions[fn.Id]; current != nil { + if modelOwnedPackages[modelPkg.Id] { + current.AnonFunctionIds = fn.AnonFunctionIds + } + continue + } + if modelOwnedPackages[modelPkg.Id] { + fn.IsSynthetic = true + fn.SyntheticKind = "opentaint model support" + } + basePkg.Functions = append(basePkg.Functions, fn) + existingFunctions[fn.Id] = fn + } + mergeGlobals(&basePkg.Globals, modelPkg.Globals) + mergeConsts(&basePkg.Constants, modelPkg.Constants) + } + + baseIndex = indexProto(base, nil) + for _, body := range model.FunctionBodies { + if modelInitFunctions[body.FunctionId] { + continue + } + fn := baseIndex.funcs[body.FunctionId] + if fn == nil || !modelOwnedPackages[fn.PackageId] { + continue + } + if previous, exists := state.modeledFunctions[body.FunctionId]; exists { + return fmt.Errorf("Go function %q is modeled more than once (%s and %s)", fn.FullName, previous, source) + } + state.modeledFunctions[body.FunctionId] = source + base.FunctionBodies = replaceFunctionBody(base.FunctionBodies, body) + fn.HasBody = true + } + + return nil +} + +func modelOwnedPackage(index *protoIndex, norm *modelPathNormalizer, packageID int32) bool { + pkg := index.packages[packageID] + if pkg == nil { + return false + } + _, ok := norm.replacements[pkg.ImportPath] + return ok +} + +func appendUniqueIDs(dst []int32, values ...int32) []int32 { + seen := make(map[int32]bool, len(dst)+len(values)) + for _, value := range dst { + seen[value] = true + } + for _, value := range values { + if value != 0 && !seen[value] { + dst = append(dst, value) + seen[value] = true + } + } + return dst +} + +func namedByID(values []*pb.ProtoNamedType) map[int32]*pb.ProtoNamedType { + result := make(map[int32]*pb.ProtoNamedType, len(values)) + for _, value := range values { + result[value.Id] = value + } + return result +} + +func functionsByID(values []*pb.ProtoFunction) map[int32]*pb.ProtoFunction { + result := make(map[int32]*pb.ProtoFunction, len(values)) + for _, value := range values { + result[value.Id] = value + } + return result +} + +func mergeGlobals(dst *[]*pb.ProtoGlobal, values []*pb.ProtoGlobal) { + seen := make(map[int32]bool, len(*dst)) + for _, value := range *dst { + seen[value.Id] = true + } + for _, value := range values { + if !seen[value.Id] { + *dst = append(*dst, value) + seen[value.Id] = true + } + } +} + +func mergeConsts(dst *[]*pb.ProtoConst, values []*pb.ProtoConst) { + seen := make(map[int32]bool, len(*dst)) + for _, value := range *dst { + seen[value.Id] = true + } + for _, value := range values { + if !seen[value.Id] { + *dst = append(*dst, value) + seen[value.Id] = true + } + } +} + +func replaceFunctionBody(values []*pb.ProtoFunctionBody, replacement *pb.ProtoFunctionBody) []*pb.ProtoFunctionBody { + for idx, body := range values { + if body.FunctionId == replacement.FunctionId { + values[idx] = replacement + return values + } + } + return append(values, replacement) +} diff --git a/core/opentaint-ir/go/go-ssa-server/model_workspace.go b/core/opentaint-ir/go/go-ssa-server/model_workspace.go new file mode 100644 index 0000000000..f04160d247 --- /dev/null +++ b/core/opentaint-ir/go/go-ssa-server/model_workspace.go @@ -0,0 +1,89 @@ +package server + +import ( + "fmt" + "go/version" + "os" + "path/filepath" + "runtime" + "sort" + "strconv" + "strings" + + "golang.org/x/tools/go/packages" +) + +func createModelWorkspace( + modelDir string, + projectInfo map[string]*packages.Package, + projectModulePaths map[string]bool, +) (string, func(), error) { + moduleDirs := make(map[string]bool) + modelDir, err := filepath.Abs(modelDir) + if err != nil { + return "", nil, fmt.Errorf("resolve Go model directory: %w", err) + } + moduleDirs[modelDir] = true + + for _, pkg := range projectInfo { + module := pkg.Module + if module == nil || module.Dir == "" || (!module.Main && !projectModulePaths[module.Path]) { + continue + } + moduleDir, absErr := filepath.Abs(module.Dir) + if absErr != nil { + return "", nil, fmt.Errorf("resolve project module %s: %w", module.Path, absErr) + } + moduleDirs[moduleDir] = true + } + + dirs := make([]string, 0, len(moduleDirs)) + for dir := range moduleDirs { + dirs = append(dirs, dir) + } + sort.Strings(dirs) + + tempDir, err := os.MkdirTemp("", "opentaint-go-model-work-*") + if err != nil { + return "", nil, fmt.Errorf("create Go model workspace: %w", err) + } + cleanup := func() { _ = os.RemoveAll(tempDir) } + + goVersion := runtime.Version() + if !version.IsValid(goVersion) { + goVersion = "go1.25.0" + } + + var workspace strings.Builder + fmt.Fprintf(&workspace, "go %s\n\nuse (\n", strings.TrimPrefix(goVersion, "go")) + for _, dir := range dirs { + fmt.Fprintf(&workspace, "\t%s\n", strconv.Quote(filepath.ToSlash(dir))) + } + workspace.WriteString(")\n") + + workspacePath := filepath.Join(tempDir, "go.work") + if err := os.WriteFile(workspacePath, []byte(workspace.String()), 0o600); err != nil { + cleanup() + return "", nil, fmt.Errorf("write Go model workspace: %w", err) + } + return workspacePath, cleanup, nil +} + +func modelModulePaths(modelDir string, modelInfo map[string]*packages.Package) map[string]bool { + result := make(map[string]bool) + modelDir, err := filepath.Abs(modelDir) + if err != nil { + return result + } + modelDir = filepath.Clean(modelDir) + for _, pkg := range modelInfo { + if pkg.Module == nil || pkg.Module.Dir == "" { + continue + } + moduleDir, absErr := filepath.Abs(pkg.Module.Dir) + if absErr == nil && filepath.Clean(moduleDir) == modelDir { + result[pkg.Module.Path] = true + } + } + return result +} diff --git a/core/opentaint-ir/go/go-ssa-server/serializer.go b/core/opentaint-ir/go/go-ssa-server/serializer.go index acaa181e07..71e2ccbc01 100644 --- a/core/opentaint-ir/go/go-ssa-server/serializer.go +++ b/core/opentaint-ir/go/go-ssa-server/serializer.go @@ -67,6 +67,7 @@ type serializer struct { mode pb.LoadMode projectModulePaths map[string]bool + onlyListedModules bool loadInfo map[string]*packages.Package } @@ -92,6 +93,21 @@ func newSerializerForBuild(prog *ssa.Program, pkgs []*ssa.Package, info map[stri return s } +func newSerializerForModelBuild( + prog *ssa.Program, + pkgs []*ssa.Package, + info map[string]*packages.Package, + projectModulePaths map[string]bool, +) *serializer { + s := newSerializerWithIDs(prog, pkgs, newIDAllocator()) + s.mode = pb.LoadMode_LOAD_MODE_PROJECT + s.projectModulePaths = projectModulePaths + s.onlyListedModules = true + s.loadInfo = info + s.collectAll() + return s +} + func (s *serializer) collectAll() { for _, pkg := range s.pkgs { s.ids.packageID(pkg) @@ -151,7 +167,7 @@ func (s *serializer) shouldEmitBody(fn *ssa.Function) bool { if dp == nil { return fn.Package() == nil } - isStdlib, isDep := classifyPackage(s.loadInfo[dp.Pkg.Path()], s.projectModulePaths) + isStdlib, isDep := classifyPackage(s.loadInfo[dp.Pkg.Path()], s.projectModulePaths, s.onlyListedModules) return !isDep && !isStdlib } @@ -168,7 +184,7 @@ func (s *serializer) serializeProgram() (*pb.ProtoProgram, []*pb.ProtoError) { protoPkgBySSA := make(map[*ssa.Package]*pb.ProtoPackage, len(s.pkgs)) for _, pkg := range s.pkgs { pp := s.serializePackage(pkg) - isStdlib, isDep := classifyPackage(s.loadInfo[pkg.Pkg.Path()], s.projectModulePaths) + isStdlib, isDep := classifyPackage(s.loadInfo[pkg.Pkg.Path()], s.projectModulePaths, s.onlyListedModules) pp.IsStdlib = isStdlib pp.IsDependency = isDep protoPkgBySSA[pkg] = pp diff --git a/core/opentaint-ir/go/go-ssa-server/server.go b/core/opentaint-ir/go/go-ssa-server/server.go index 454a3e164a..4b690b2bdc 100644 --- a/core/opentaint-ir/go/go-ssa-server/server.go +++ b/core/opentaint-ir/go/go-ssa-server/server.go @@ -91,17 +91,83 @@ func (s *goSSAServer) BuildProgram(req *pb.BuildProgramRequest, stream pb.GoSSAS } } + mergeState := newModelMergeState() + for _, modelDir := range req.ModelDirs { + workspacePath, cleanupWorkspace, workspaceErr := createModelWorkspace( + modelDir, info, projectModulePaths, + ) + if workspaceErr != nil { + return stream.Send(&pb.BuildProgramResponse{Payload: &pb.BuildProgramResponse_Error{Error: &pb.ProtoError{ + Message: fmt.Sprintf("loading Go model %s: %v", modelDir, workspaceErr), + Fatal: true, + }}}) + } + modelReq := &pb.BuildProgramRequest{ + Patterns: []string{"./..."}, + InstantiateGenerics: req.InstantiateGenerics, + SanityCheck: req.SanityCheck, + Gopath: req.Gopath, + Goroot: req.Goroot, + BuildTags: req.BuildTags, + WorkingDir: modelDir, + Mode: pb.LoadMode_LOAD_MODE_PROJECT, + } + modelProg, modelPkgs, modelInfo, modelErr := buildSSAWithPackageErrorsAndEnv( + modelReq, true, []string{"GOWORK=" + workspacePath}, + ) + cleanupWorkspace() + if modelErr != nil { + return stream.Send(&pb.BuildProgramResponse{Payload: &pb.BuildProgramResponse_Error{Error: &pb.ProtoError{ + Message: fmt.Sprintf("loading Go model %s: %v", modelDir, modelErr), + Fatal: true, + }}}) + } + modelSerializer := newSerializerForModelBuild( + modelProg, modelPkgs, modelInfo, modelModulePaths(modelDir, modelInfo), + ) + modelProgram, modelSerializeErrors := modelSerializer.serializeProgram() + for _, modelSerializeErr := range modelSerializeErrors { + if err := stream.Send(&pb.BuildProgramResponse{Payload: &pb.BuildProgramResponse_Error{Error: modelSerializeErr}}); err != nil { + return err + } + } + if err := mergeGoModel(program, modelProgram, mergeState, modelDir); err != nil { + return stream.Send(&pb.BuildProgramResponse{Payload: &pb.BuildProgramResponse_Error{Error: &pb.ProtoError{ + Message: err.Error(), + Fatal: true, + }}}) + } + } + if err := stream.Send(&pb.BuildProgramResponse{Payload: &pb.BuildProgramResponse_Program{Program: program}}); err != nil { return fmt.Errorf("streaming program: %w", err) } return stream.Send(&pb.BuildProgramResponse{ Payload: &pb.BuildProgramResponse_Summary{Summary: &pb.ProtoBuildSummary{ - PackageCount: int32(ser.stats.packageCount), - FunctionCount: int32(ser.stats.functionCount), - TypeCount: int32(ser.stats.typeCount), - InstructionCount: int32(ser.stats.instructionCount), + PackageCount: int32(len(program.Packages)), + FunctionCount: countProgramFunctions(program), + TypeCount: int32(len(program.Types)), + InstructionCount: countProgramInstructions(program), BuildTimeMs: time.Since(startTime).Milliseconds(), }}, }) } + +func countProgramFunctions(program *pb.ProtoProgram) int32 { + var count int32 + for _, pkg := range program.Packages { + count += int32(len(pkg.Functions)) + } + return count +} + +func countProgramInstructions(program *pb.ProtoProgram) int32 { + var count int32 + for _, body := range program.FunctionBodies { + for _, block := range body.Blocks { + count += int32(len(block.Instructions)) + } + } + return count +} diff --git a/core/opentaint-ir/go/proto/goir/service.proto b/core/opentaint-ir/go/proto/goir/service.proto index 8a851d0a7f..c90833d73a 100644 --- a/core/opentaint-ir/go/proto/goir/service.proto +++ b/core/opentaint-ir/go/proto/goir/service.proto @@ -36,6 +36,7 @@ message BuildProgramRequest { string working_dir = 8; LoadMode mode = 11; repeated string project_module_paths = 12; + repeated string model_dirs = 13; } message BuildProgramResponse { diff --git a/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/Approximations.kt b/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/Approximations.kt index 43c3c4a33d..be4854dd57 100644 --- a/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/Approximations.kt +++ b/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/Approximations.kt @@ -24,6 +24,7 @@ import org.opentaint.ir.api.storage.ers.EntityIterable import org.opentaint.ir.api.storage.ers.compressed import org.opentaint.ir.approximation.annotation.Approximate import org.opentaint.ir.approximation.annotation.ApproximateByName +import org.opentaint.ir.approximation.annotation.ApproximatedClassName import org.opentaint.ir.approximation.annotation.ApproximatedFieldName import org.opentaint.ir.approximation.annotation.ApproximatedMethodName import org.opentaint.ir.approximation.annotation.Version @@ -98,6 +99,7 @@ class Approximations( val persistence = signal.jIRdb.persistence val approxSymbolClass = persistence.findSymbolId(approximationAnnotationClassName) val approxSymbolName = persistence.findSymbolId(approximationByNameAnnotationClassName) + val patchedClassSymbolName = persistence.findSymbolId(approximatedClassName) persistence.read { context -> context.execute( sqlAction = { @@ -147,7 +149,36 @@ class Approximations( clazz.getCompressed("nameId") to annotationValue.getCompressedBlob("classSymbolId") } - namedApprox + classApprox + val patchedClassApprox = context.txn.find("Annotation", "nameId", patchedClassSymbolName.compressed) + .filter { it.getCompressedBlob("refKind") == RefKind.CLASS.ordinal } + .mapNotNull { annotation -> + matchAnnotationVersions(persistence, annotation, versionsId, versionSymbol, targetId, fromVersionId, toVersionId) + }.mapNotNull { (annotation, values) -> + val clazz = annotation.getLink("ref") + val approximationNameId = clazz.getCompressed("nameId") + ?: return@mapNotNull null + val approximationName = persistence.findSymbolName(approximationNameId) + val patchedName = values.annotationStringValueByName(persistence, valueId) + ?: return@mapNotNull null + val originalName = prefixAddressedTarget(approximationName, patchedName) + ?: error("@$approximatedClassName requires an $approximationPackagePrefix package") + + approximationNameId to persistence.findSymbolId(originalName) + } + + val explicitMappings = (namedApprox + classApprox + patchedClassApprox).toList() + val explicitlyMappedClasses = explicitMappings.mapTo(hashSetOf()) { it.first } + val prefixMappings = context.txn.all("Class").mapNotNull { clazz -> + val approximationNameId = clazz.getCompressed("nameId") + ?: return@mapNotNull null + if (approximationNameId in explicitlyMappedClasses) return@mapNotNull null + val originalName = prefixAddressedTarget(persistence.findSymbolName(approximationNameId)) + ?: return@mapNotNull null + + approximationNameId to persistence.findSymbolId(originalName) + } + + explicitMappings.asSequence() + prefixMappings } ).forEach { (approximation, original) -> val approximationClassName = persistence.findSymbolName(approximation!!).toApproximationName() @@ -361,19 +392,40 @@ private class ApproximationIndexer( } override fun index(classNode: ClassNode) { - val annotations = classNode.visibleAnnotations ?: return - - // Check whether the classNode contains an approximation related annotation - val approximationAnnotation = annotations.singleOrNull { - approximationAnnotationClassName in it.desc.className || approximationByNameAnnotationClassName in it.desc.className - } ?: return + val approximationClassName = classNode.name.className + val prefixTarget = prefixAddressedTarget(approximationClassName) + val approximationAnnotations = classNode.visibleAnnotations.orEmpty().filter { + approximationAnnotationClassName in it.desc.className || + approximationByNameAnnotationClassName in it.desc.className || + approximatedClassName in it.desc.className + } + if (prefixTarget == null && approximationAnnotations.isEmpty()) return + if (approximationAnnotations.size > 1) { + logger.error { "Approximation $approximationClassName has more than one class-address annotation" } + return + } + val approximationAnnotation = approximationAnnotations.singleOrNull() - if (!checkVersion(approximationAnnotation)) + if (approximationAnnotation != null && !checkVersion(approximationAnnotation)) return val originalClassName = when { + approximationAnnotation == null -> prefixTarget!!.toOriginalName() + + approximatedClassName in approximationAnnotation.desc.className -> { + val patchedName = annotationStringValueByName(approximationAnnotation.values, "value") + ?: error("unable to find `value` in @$approximatedClassName") + val target = prefixAddressedTarget(approximationClassName, patchedName) + if (target == null) { + logger.error { "@$approximatedClassName on $approximationClassName requires an $approximationPackagePrefix package" } + return + } + target.toOriginalName() + } + approximationByNameAnnotationClassName in approximationAnnotation.desc.className -> { - val target = approximationAnnotation.values[1] as String + val target = annotationStringValueByName(approximationAnnotation.values, "value") + ?: error("unable to find `value` in @$approximationByNameAnnotationClassName") target.toOriginalName() } @@ -386,12 +438,12 @@ private class ApproximationIndexer( else -> error("Impossible") } - val approximationClassName = classNode.name.className.toApproximationName() + val typedApproximationClassName = approximationClassName.toApproximationName() // Ensure that each approximation has one and only one - if (originalToApproximation.getOrDefault(originalClassName, approximationClassName) != approximationClassName) { + if (originalToApproximation.getOrDefault(originalClassName, typedApproximationClassName) != typedApproximationClassName) { logger.error { - "An error occurred during approximations indexing: you tried to add `$approximationClassName` " + + "An error occurred during approximations indexing: you tried to add `$typedApproximationClassName` " + "as an approximation for `$originalClassName`, but the target class is already " + "associated with approximation `${originalToApproximation[originalClassName]}`. " + "Only bijection between classes is allowed." @@ -399,18 +451,18 @@ private class ApproximationIndexer( return } - if (approximationToOriginal.getOrDefault(approximationClassName, originalClassName) != originalClassName) { + if (approximationToOriginal.getOrDefault(typedApproximationClassName, originalClassName) != originalClassName) { logger.error { - "An error occurred during approximations indexing: you tried to add `$approximationClassName` " + + "An error occurred during approximations indexing: you tried to add `$typedApproximationClassName` " + "as an approximation for `$originalClassName`, but this approximation is already used for " + - "`${approximationToOriginal[approximationClassName]}`. " + + "`${approximationToOriginal[typedApproximationClassName]}`. " + "Only bijection between classes is allowed." } return } - originalToApproximation[originalClassName] = approximationClassName - approximationToOriginal[approximationClassName] = originalClassName + originalToApproximation[originalClassName] = typedApproximationClassName + approximationToOriginal[typedApproximationClassName] = originalClassName } override fun flush(context: StorageContext) { @@ -424,10 +476,25 @@ private class ApproximationIndexer( private val approximationAnnotationClassName = Approximate::class.qualifiedName!! private val approximationByNameAnnotationClassName = ApproximateByName::class.qualifiedName!! +private val approximatedClassName = ApproximatedClassName::class.qualifiedName!! private val approximatedMethodName = ApproximatedMethodName::class.qualifiedName!! private val approximatedFieldName = ApproximatedFieldName::class.qualifiedName!! private val versionAnnotationClassName = Version::class.qualifiedName!! +private const val approximationPackagePrefix = "opentaint." + +private fun prefixAddressedTarget(approximationClassName: String, patchedClassName: String? = null): String? { + if (!approximationClassName.startsWith(approximationPackagePrefix)) return null + val defaultTarget = approximationClassName.removePrefix(approximationPackagePrefix) + if (defaultTarget.isEmpty()) return null + if (patchedClassName == null) return defaultTarget + require(patchedClassName.isNotBlank() && '.' !in patchedClassName) { + "Approximated class name must be one non-empty JVM class-name component: $patchedClassName" + } + val targetPackage = defaultTarget.substringBeforeLast('.', missingDelimiterValue = "") + return if (targetPackage.isEmpty()) patchedClassName else "$targetPackage.$patchedClassName" +} + @JvmInline value class ApproximationClassName(val className: String) { override fun toString(): String = className diff --git a/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/annotation/ApproximationAnnotations.kt b/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/annotation/ApproximationAnnotations.kt index 24d73bdaff..3656a722df 100644 --- a/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/annotation/ApproximationAnnotations.kt +++ b/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/annotation/ApproximationAnnotations.kt @@ -20,6 +20,17 @@ annotation class ApproximateByName( val versions: Array = [], ) +/** + * Sets the final JVM class name for a prefix-addressed approximation. + * The `opentaint.` package sets the target package. + */ +@Suppress("unused") +@Target(AnnotationTarget.CLASS) +annotation class ApproximatedClassName( + val value: String, + val versions: Array = [], +) + @Suppress("unused") @Target(AnnotationTarget.FUNCTION) annotation class ApproximatedMethodName(val value: String) diff --git a/core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/java/lang/Integer.java b/core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/java/lang/Integer.java new file mode 100644 index 0000000000..6d094a0fa9 --- /dev/null +++ b/core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/java/lang/Integer.java @@ -0,0 +1,17 @@ +package opentaint.java.lang; + +public class Integer { + private final int value; + + public Integer(int value) { + this.value = value; + } + + public static Integer valueOf(int value) { + return new Integer(value); + } + + public int getValue() { + return value; + } +} diff --git a/core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/org/opentaint/ir/approximations/target/KotlinClassName.java b/core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/org/opentaint/ir/approximations/target/KotlinClassName.java new file mode 100644 index 0000000000..9ccdf15030 --- /dev/null +++ b/core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/org/opentaint/ir/approximations/target/KotlinClassName.java @@ -0,0 +1,10 @@ +package opentaint.org.opentaint.ir.approximations.target; + +import org.opentaint.ir.approximation.annotation.ApproximatedClassName; + +@ApproximatedClassName("Kotlin-Class") +public class KotlinClassName { + public int evaluate(int value) { + return value; + } +} diff --git a/core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/org/opentaint/ir/approximations/target/KotlinNames.java b/core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/org/opentaint/ir/approximations/target/KotlinNames.java new file mode 100644 index 0000000000..7fa17c26b7 --- /dev/null +++ b/core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/org/opentaint/ir/approximations/target/KotlinNames.java @@ -0,0 +1,14 @@ +package opentaint.org.opentaint.ir.approximations.target; + +import org.opentaint.ir.approximation.annotation.ApproximatedFieldName; +import org.opentaint.ir.approximation.annotation.ApproximatedMethodName; + +public class KotlinNames { + @ApproximatedFieldName("field-with-dash") + public int fieldWithDash; + + @ApproximatedMethodName("method-with-dash") + public int methodWithDash(int value) { + return value; + } +} diff --git a/core/opentaint-ir/opentaint-ir-approximations/src/test/java/org/opentaint/ir/approximations/approx/IntegerApprox.java b/core/opentaint-ir/opentaint-ir-approximations/src/test/java/org/opentaint/ir/approximations/approx/IntegerApprox.java deleted file mode 100644 index 22a28e859e..0000000000 --- a/core/opentaint-ir/opentaint-ir-approximations/src/test/java/org/opentaint/ir/approximations/approx/IntegerApprox.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.opentaint.ir.approximations.approx; - -import org.opentaint.ir.approximation.annotation.Approximate; - - -@Approximate(java.lang.Integer.class) -public class IntegerApprox { - private final int value; - - public IntegerApprox(int value) { - this.value = value; - } - - public static IntegerApprox valueOf(int value) { - return new IntegerApprox(value); - } - - public int getValue() { - return value; - } -} diff --git a/core/opentaint-ir/opentaint-ir-approximations/src/test/java/org/opentaint/ir/approximations/legacy/LegacyTargetApprox.java b/core/opentaint-ir/opentaint-ir-approximations/src/test/java/org/opentaint/ir/approximations/legacy/LegacyTargetApprox.java new file mode 100644 index 0000000000..978840e742 --- /dev/null +++ b/core/opentaint-ir/opentaint-ir-approximations/src/test/java/org/opentaint/ir/approximations/legacy/LegacyTargetApprox.java @@ -0,0 +1,11 @@ +package org.opentaint.ir.approximations.legacy; + +import org.opentaint.ir.approximation.annotation.Approximate; +import org.opentaint.ir.approximations.target.LegacyTarget; + +@Approximate(LegacyTarget.class) +public class LegacyTargetApprox { + public int identity(int value) { + return value; + } +} diff --git a/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/approx/KotlinClassApprox.kt b/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/opentaint/org/opentaint/ir/approximations/target/KotlinClass.kt similarity index 72% rename from core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/approx/KotlinClassApprox.kt rename to core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/opentaint/org/opentaint/ir/approximations/target/KotlinClass.kt index 47240c4c9e..d82b7cf254 100644 --- a/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/approx/KotlinClassApprox.kt +++ b/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/opentaint/org/opentaint/ir/approximations/target/KotlinClass.kt @@ -1,17 +1,16 @@ -package org.opentaint.ir.approximations.approx +package opentaint.org.opentaint.ir.approximations.target -import org.opentaint.ir.approximation.annotation.Approximate +import opentaint.java.lang.Integer as IntegerModel import org.opentaint.ir.approximations.target.ClassForField -import org.opentaint.ir.approximations.target.KotlinClass +import org.opentaint.ir.approximations.target.KotlinClass as TargetKotlinClass import org.jetbrains.annotations.NotNull -@Approximate(KotlinClass::class) -class KotlinClassApprox { +class KotlinClass { @NotNull private val artificialField: ClassForField = ClassForField() private val fieldToReplace: Int = 3 - private val sameApproximation: KotlinClassApprox? = null - private val anotherApproximation: IntegerApprox? = null + private val sameApproximation: KotlinClass? = null + private val anotherApproximation: IntegerModel? = null fun replaceBehaviour(value: Int): Int = 42 @@ -25,7 +24,7 @@ class KotlinClassApprox { return 0 } - fun useSameApproximationTarget(kotlinClass: KotlinClass): Int { + fun useSameApproximationTarget(kotlinClass: TargetKotlinClass): Int { if (sameApproximation == null) return 0 if (kotlinClass.methodWithoutApproximation() == sameApproximation.artificialMethod()) { @@ -52,4 +51,4 @@ class KotlinClassApprox { return 2 } -} \ No newline at end of file +} diff --git a/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/ApproximationsTest.kt b/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/ApproximationsTest.kt index c87adc8d5c..008c6d6133 100644 --- a/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/ApproximationsTest.kt +++ b/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/ApproximationsTest.kt @@ -3,11 +3,13 @@ package org.opentaint.ir.approximations import org.opentaint.ir.api.jvm.JavaVersion import org.opentaint.ir.api.jvm.LocationType import org.opentaint.ir.api.jvm.cfg.JIRAssignInst +import org.opentaint.ir.api.jvm.cfg.JIRArgument import org.opentaint.ir.api.jvm.cfg.JIRCallInst import org.opentaint.ir.api.jvm.cfg.JIRFieldRef import org.opentaint.ir.api.jvm.cfg.JIRRawAssignInst import org.opentaint.ir.api.jvm.cfg.JIRRawCallInst import org.opentaint.ir.api.jvm.cfg.JIRRawFieldRef +import org.opentaint.ir.api.jvm.cfg.JIRReturnInst import org.opentaint.ir.api.jvm.ext.findClass import org.opentaint.ir.api.jvm.ext.findDeclaredFieldOrNull import org.opentaint.ir.approximation.Approximations @@ -16,6 +18,8 @@ import org.opentaint.ir.approximation.JIREnrichedVirtualMethod import org.opentaint.ir.approximation.toApproximationName import org.opentaint.ir.approximation.toOriginalName import org.opentaint.ir.approximations.target.KotlinClass +import org.opentaint.ir.approximations.target.KotlinNames +import org.opentaint.ir.approximations.target.LegacyTarget import org.opentaint.ir.impl.fs.JarLocation import org.opentaint.ir.testing.BaseTest import org.opentaint.ir.testing.WithDb @@ -45,6 +49,7 @@ open class ApproximationsTest : BaseTest() { val approximation = approximations.findApproximationByOriginOrNull(originalClassName) assertNotNull(approximation) + assertEquals("opentaint.org.opentaint.ir.approximations.target.KotlinClass", approximation) assertEquals(classes.name, approximations.findOriginalByApproximationOrNull(approximation!!.toApproximationName())) } @@ -57,9 +62,19 @@ open class ApproximationsTest : BaseTest() { val approximation = approximations.findApproximationByOriginOrNull(originalClassName) assertNotNull(approximation) + assertEquals("opentaint.java.lang.Integer", approximation) assertEquals(classec.name, approximations.findOriginalByApproximationOrNull(approximation!!.toApproximationName())) } + @Test + fun `legacy annotation addressing remains supported`() { + val target = cp.findClass() + val approximation = approximations.findApproximationByOriginOrNull(target.name.toOriginalName()) + + assertEquals("org.opentaint.ir.approximations.legacy.LegacyTargetApprox", approximation) + assertTrue(target.declaredMethods.single { it.name == "identity" } is JIREnrichedVirtualMethod) + } + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") @Test fun `integer approximation`() { @@ -126,6 +141,40 @@ open class ApproximationsTest : BaseTest() { assertEquals(methods, classec.declaredMethods) } + @Test + fun `java approximation patches kotlin names that java cannot express`() { + val clazz = cp.findClass() + + val field = clazz.declaredFields.single { it.name == "field-with-dash" } + assertTrue(field is JIREnrichedVirtualField) + + val method = clazz.declaredMethods.single { it.name == "method-with-dash" } + assertTrue(method is JIREnrichedVirtualMethod) + + val returnInstruction = method.flowGraph().instructions + .flatMap { instruction -> + when (instruction) { + is JIRReturnInst -> listOf(instruction) + else -> emptyList() + } + } + .single() + val returnedArgument = returnInstruction.returnValue as JIRArgument + assertEquals(0, returnedArgument.index) + assertEquals("value", returnedArgument.name) + } + + @Test + fun `java approximation patches kotlin class name that java cannot express`() { + val clazz = cp.findClass("org.opentaint.ir.approximations.target.Kotlin-Class") + val method = clazz.declaredMethods.single { it.name == "evaluate" } + + assertTrue(method is JIREnrichedVirtualMethod) + val returnInstruction = method.flowGraph().instructions.filterIsInstance().single() + val returnedArgument = returnInstruction.returnValue as JIRArgument + assertEquals(0, returnedArgument.index) + } + @Test fun `replace approximations in methodBody`() { val classec = cp.findClass() @@ -136,7 +185,7 @@ open class ApproximationsTest : BaseTest() { val rawInstructions = method.rawInstList assertTrue(method.enclosingClass === classec) - assertTrue("KotlinClassApprox" !in method.description) + assertTrue("opentaint." !in method.description) val types = hashSetOf() diff --git a/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/target/KotlinNames.kt b/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/target/KotlinNames.kt new file mode 100644 index 0000000000..434cb82af7 --- /dev/null +++ b/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/target/KotlinNames.kt @@ -0,0 +1,16 @@ +package org.opentaint.ir.approximations.target + +class KotlinNames { + @JvmField + val `field-with-dash`: Int = -1 + + fun `method-with-dash`(value: Int): Int = -value +} + +class `Kotlin-Class` { + fun evaluate(value: Int): Int = -value +} + +class LegacyTarget { + fun identity(value: Int): Int = -value +} diff --git a/core/src/main/kotlin/org/opentaint/go/sast/project/AnalysisCtx.kt b/core/src/main/kotlin/org/opentaint/go/sast/project/AnalysisCtx.kt index 8cfb8c73b8..544b7ade0c 100644 --- a/core/src/main/kotlin/org/opentaint/go/sast/project/AnalysisCtx.kt +++ b/core/src/main/kotlin/org/opentaint/go/sast/project/AnalysisCtx.kt @@ -6,17 +6,22 @@ import org.opentaint.ir.go.client.GoIRClient import org.opentaint.ir.go.client.GoIRLoadConfig import org.opentaint.ir.go.client.GoIRLoadMode import org.opentaint.project.GoProject +import java.nio.file.Path class AnalysisCtx( private val prj: GoProject, val client: GoIRClient, + private val modelPaths: List = emptyList(), ) : AutoCloseable by client { val cp: GoIRProgram by lazy { logger.info { "Building Go IR for project: ${prj.projectDir}" } - client.buildFromDir(prj.projectDir, GoIRLoadConfig(mode = GoIRLoadMode.PROJECT)).program + client.buildFromDir( + prj.projectDir, + GoIRLoadConfig(mode = GoIRLoadMode.PROJECT, modelDirs = modelPaths), + ).program } companion object { private val logger = object : KLogging() {}.logger } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalysisOptions.kt b/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalysisOptions.kt index 354919aa61..52d9d1081b 100644 --- a/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalysisOptions.kt +++ b/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalysisOptions.kt @@ -1,7 +1,9 @@ package org.opentaint.go.sast.project import org.opentaint.common.sast.CommonAnalysisOptions +import java.nio.file.Path data class GoProjectAnalysisOptions( val common: CommonAnalysisOptions = CommonAnalysisOptions(), + val modelPaths: List = emptyList(), ) diff --git a/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalyzer.kt b/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalyzer.kt index d8c026065d..f469ec8202 100644 --- a/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalyzer.kt +++ b/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalyzer.kt @@ -30,9 +30,11 @@ class GoProjectAnalyzer( results, goOptions.common ) { + private val modelPaths = goOptions.modelPaths + override fun initializeProjectAnalysisContext(): AnalysisCtx { val client = GoIRClient() - return AnalysisCtx(project, client) + return AnalysisCtx(project, client, modelPaths) } override fun AnalysisCtx.selectProjectEntryPoints(): List { diff --git a/core/src/main/kotlin/org/opentaint/go/sast/project/GoTestProjectAnalyzer.kt b/core/src/main/kotlin/org/opentaint/go/sast/project/GoTestProjectAnalyzer.kt index bd1d834d66..7841f870f4 100644 --- a/core/src/main/kotlin/org/opentaint/go/sast/project/GoTestProjectAnalyzer.kt +++ b/core/src/main/kotlin/org/opentaint/go/sast/project/GoTestProjectAnalyzer.kt @@ -43,6 +43,7 @@ class GoTestProjectAnalyzer( ) : TestProjectAnalyzerBase( project, results, providedOptions.common.copy(storeSummaries = false), ) { + private val modelPaths = providedOptions.modelPaths private val loadedRules = options.loadSemgrepRules(GoLanguageStrategy()) @Serializable @@ -77,7 +78,7 @@ class GoTestProjectAnalyzer( Yaml().decodeFromStream(it) } - val ctx = AnalysisCtx(project, GoIRClient()) + val ctx = AnalysisCtx(project, GoIRClient(), modelPaths) val results = ctx.analyzeTestSamples(ruleTests.tests) ?: return ProjectAnalysisStatus.EXCEPTION diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/runner/ProjectAnalyzerRunner.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/runner/ProjectAnalyzerRunner.kt index d2f3533df7..0610fad009 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/runner/ProjectAnalyzerRunner.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/runner/ProjectAnalyzerRunner.kt @@ -61,6 +61,10 @@ class ProjectAnalyzerRunner : AbstractAnalyzerRunner() { .directory() .multiple() + private val goModels: List by option(help = "Directory containing opentaint/ Go model sources") + .directory() + .multiple() + private val semgrepRuleLoadTrace: Path? by option(help = "Output file for Semgrep rules loader trace") .newFile() @@ -126,7 +130,10 @@ class ProjectAnalyzerRunner : AbstractAnalyzerRunner() { ), ) - override fun goOptions() = GoProjectAnalysisOptions(common = commonOptions) + override fun goOptions() = GoProjectAnalysisOptions( + common = commonOptions, + modelPaths = goModels, + ) private fun collectYamlConfigs(path: Path): List = path.walk() .filter { it.extension in arrayOf("yaml", "yml") } From 6e834c42ac8e832b6b952193de959478290a894d Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Fri, 28 Aug 2026 14:46:50 +0200 Subject: [PATCH 02/12] refactor(go-models): resolve models in Kotlin client --- .../ir/go/client/GoIRDeserializer.kt | 13 +- .../opentaint/ir/go/client/GoModelMerger.kt | 986 +++++++++++++++ .../ir/go/test/features/ModelTests.kt | 265 +++++ .../go/go-ssa-server/build_program_test.go | 952 +++------------ .../go/go-ssa-server/model_merge.go | 1053 ----------------- core/opentaint-ir/go/go-ssa-server/server.go | 17 +- core/opentaint-ir/go/proto/goir/service.proto | 7 + 7 files changed, 1449 insertions(+), 1844 deletions(-) create mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt delete mode 100644 core/opentaint-ir/go/go-ssa-server/model_merge.go diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt index 0e664b7254..bcd541a8d7 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt @@ -81,6 +81,7 @@ import org.opentaint.ir.go.proto.ProtoConstValue import org.opentaint.ir.go.proto.ProtoFunction import org.opentaint.ir.go.proto.ProtoFunctionBody import org.opentaint.ir.go.proto.ProtoInstruction +import org.opentaint.ir.go.proto.ProtoModelProgram import org.opentaint.ir.go.proto.ProtoNamedType import org.opentaint.ir.go.proto.ProtoNamedTypeKind import org.opentaint.ir.go.proto.ProtoPackage @@ -136,12 +137,15 @@ class GoIRDeserializer { fun deserialize(responses: Iterator): GoIRProgram { var program: ProtoProgram? = null + val models = mutableListOf() for (r in responses) { when (r.payloadCase) { BuildProgramResponse.PayloadCase.PROGRAM -> { program = r.program } + BuildProgramResponse.PayloadCase.MODEL_PROGRAM -> models += r.modelProgram + BuildProgramResponse.PayloadCase.SUMMARY -> serverBuildTimeMs = r.summary.buildTimeMs BuildProgramResponse.PayloadCase.ERROR -> handleError(r.error.message, r.error.fatal) BuildProgramResponse.PayloadCase.PAYLOAD_NOT_SET -> {} @@ -152,24 +156,25 @@ class GoIRDeserializer { } check(program != null) { "Unexpected IR build issue" } + val mergedProgram = GoModelMerger().merge(program, models) val resultPackages = hashMapOf() val anonymousInterfaces = Int2ObjectOpenHashMap() val result = GoIRProgramImpl(resultPackages, anonymousInterfaces) - val pkgInfo = deserializePackages(program.packagesList) + val pkgInfo = deserializePackages(mergedProgram.packagesList) packages.filterNotNull().forEach { resultPackages[it.importPath] = it } - TypeDeserializationCtx(result, program.typesList, pkgInfo, anonymousInterfaces) + TypeDeserializationCtx(result, mergedProgram.typesList, pkgInfo, anonymousInterfaces) .deserializeTypes() FunctionDeserializationCtx(pkgInfo, result) .deserializeFunctions() - program.packagesList.forEach { deserializePackageMembers(it) } - program.functionBodiesList.forEach { deserializeFunctionBody(it) } + mergedProgram.packagesList.forEach { deserializePackageMembers(it) } + mergedProgram.functionBodiesList.forEach { deserializeFunctionBody(it) } pkgInfo.namedTypes.forEach { resolveNamedTypeBindings(it) } return result diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt new file mode 100644 index 0000000000..8e5f94865d --- /dev/null +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt @@ -0,0 +1,986 @@ +package org.opentaint.ir.go.client + +import com.google.protobuf.Descriptors.FieldDescriptor +import com.google.protobuf.Message +import org.opentaint.ir.go.proto.ProtoConst +import org.opentaint.ir.go.proto.ProtoFieldDecl +import org.opentaint.ir.go.proto.ProtoFunction +import org.opentaint.ir.go.proto.ProtoFunctionBody +import org.opentaint.ir.go.proto.ProtoGlobal +import org.opentaint.ir.go.proto.ProtoModelProgram +import org.opentaint.ir.go.proto.ProtoNamedType +import org.opentaint.ir.go.proto.ProtoNamedTypeKind +import org.opentaint.ir.go.proto.ProtoPackage +import org.opentaint.ir.go.proto.ProtoProgram +import org.opentaint.ir.go.proto.ProtoStructField +import org.opentaint.ir.go.proto.ProtoStructType +import org.opentaint.ir.go.proto.ProtoTypeDefinition + +internal class GoModelMerger { + private val modeledPackages = mutableMapOf() + private val modeledFunctions = mutableMapOf() + + fun merge(base: ProtoProgram, models: List): ProtoProgram { + return models.fold(base) { program, model -> + mergeModel(program, model.program, model.source) + } + } + + private fun mergeModel(base: ProtoProgram, rawModel: ProtoProgram, source: String): ProtoProgram { + val normalizer = try { + ModelPathNormalizer.create(rawModel) + } catch (error: IllegalArgumentException) { + throw IllegalArgumentException("invalid Go model $source: ${error.message}", error) + } + + normalizer.targets.forEach { target -> + val previous = modeledPackages.putIfAbsent(target, source) + require(previous == null) { + "Go package \"$target\" is modeled more than once ($previous and $source)" + } + } + + var model = rawModel + var baseIndex = ProtoIndex(base) + var modelIndex = ProtoIndex(model, normalizer) + val fieldPlans = invalidModel(source) { + planModelStructFields(baseIndex, modelIndex, normalizer) + } + model = applyModelStructFieldPlans(model, fieldPlans) + modelIndex = ProtoIndex(model, normalizer) + model = invalidModel(source) { + remapModelStructFieldAccesses(model, modelIndex, fieldPlans) + } + modelIndex = ProtoIndex(model, normalizer) + + val ownedOldPackages = model.packagesList + .filter { it.importPath in normalizer.replacements } + .mapTo(mutableSetOf()) { it.id } + val initOldFunctions = collectInitializerFunctions(model, ownedOldPackages) + + val basePackages = baseIndex.reversePackageKeys() + model.packagesList.forEach { modelPackage -> + val target = normalizer.replacements[modelPackage.importPath] ?: return@forEach + val targetPackage = basePackages[target]?.let(baseIndex.packages::get) ?: return@forEach + require(modelPackage.name == targetPackage.name) { + "Go model package \"${modelPackage.importPath}\" has package name " + + "\"${modelPackage.name}\", want \"${targetPackage.name}\" " + + "(the modeled package name)" + } + } + + val counters = IdCounters.forProgram(base) + val modelFunctionKeys = modelIndex.functionKeysById().toMutableMap() + modelIndex.functions.forEach { (id, function) -> + if (function.parentFunctionId != 0) { + modelFunctionKeys[id] = modelFunctionKeys.getValue(id) + + "#opentaint-model-anonymous:$id" + } + } + val remap = IdRemap( + types = assignIds(modelIndex.typeKeysById(), baseIndex.reverseTypeKeys(), counters::nextType), + packages = assignIds( + modelIndex.packageKeys, + baseIndex.reversePackageKeys(), + counters::nextPackage, + ), + functions = assignIds( + modelFunctionKeys, + baseIndex.reverseFunctionKeys(), + counters::nextFunction, + ), + named = assignIds(modelIndex.namedKeys, baseIndex.reverseNamedKeys(), counters::nextNamed), + globals = assignIds( + modelIndex.globalKeysById(), + baseIndex.reverseGlobalKeys(), + counters::nextGlobal, + ), + consts = assignIds( + modelIndex.constKeysById(), + baseIndex.reverseConstKeys(), + counters::nextConst, + ), + ) + + validateFunctionSignatures( + baseIndex, + modelIndex, + normalizer, + modelFunctionKeys, + initOldFunctions, + ) + + model = normalizeModel(model, normalizer, ownedOldPackages) + val ownedPackages = ownedOldPackages.mapTo(mutableSetOf()) { remap.packages.getValue(it) } + val initFunctions = initOldFunctions.mapTo(mutableSetOf()) { remap.functions.getValue(it) } + model = try { + remapMessageIds(model, remap) as ProtoProgram + } catch (error: IllegalArgumentException) { + throw IllegalArgumentException("merging Go model $source: ${error.message}", error) + } + + var result = mergeModelStructFields(base, model, fieldPlans, remap, counters, source) + baseIndex = ProtoIndex(result) + + val missingTypes = model.typesList.filter { it.id !in baseIndex.types } + if (missingTypes.isNotEmpty()) { + result = result.toBuilder().addAllTypes(missingTypes).build() + } + + result = mergePackages(result, model, ownedPackages, initFunctions) + baseIndex = ProtoIndex(result) + result = mergeFunctionBodies(result, model, baseIndex, ownedPackages, initFunctions, source) + return result + } + + private inline fun invalidModel(source: String, action: () -> T): T { + return try { + action() + } catch (error: IllegalArgumentException) { + throw IllegalArgumentException("invalid Go model $source: ${error.message}", error) + } + } + + private fun collectInitializerFunctions( + model: ProtoProgram, + ownedPackages: Set, + ): MutableSet { + val functions = model.packagesList + .filter { it.id in ownedPackages } + .flatMap { it.functionsList } + val result = functions + .filter { it.name == "init" || it.name.startsWith("init#") } + .mapTo(mutableSetOf()) { it.id } + + do { + val size = result.size + functions.forEach { function -> + if (function.parentFunctionId in result) { + result += function.id + } + } + } while (result.size != size) + return result + } + + private fun validateFunctionSignatures( + baseIndex: ProtoIndex, + modelIndex: ProtoIndex, + normalizer: ModelPathNormalizer, + modelFunctionKeys: Map, + initFunctions: Set, + ) { + val baseFunctions = baseIndex.reverseFunctionKeys() + modelFunctionKeys.forEach { (modelId, key) -> + val baseId = baseFunctions[key] ?: return@forEach + val modelFunction = modelIndex.functions[modelId] ?: return@forEach + if ( + modelFunction.parentFunctionId != 0 || + modelId in initFunctions || + !modelIndex.isOwnedPackage(normalizer, modelFunction.packageId) + ) { + return@forEach + } + val baseFunction = baseIndex.functions.getValue(baseId) + require( + modelIndex.typeKey(modelFunction.signatureTypeId) == + baseIndex.typeKey(baseFunction.signatureTypeId) && + modelFunction.isMethod == baseFunction.isMethod && + modelFunction.isPointerReceiver == baseFunction.isPointerReceiver, + ) { + "Go model function $key has a different signature from its target" + } + } + } + + private fun normalizeModel( + model: ProtoProgram, + normalizer: ModelPathNormalizer, + ownedPackages: Set, + ): ProtoProgram { + val packages = model.packagesList.map { pkg -> + val builder = pkg.toBuilder() + if (pkg.id in ownedPackages) { + builder + .setInitFunctionId(0) + .setImportPath(normalizer.path(pkg.importPath)) + .setIsDependency(true) + .setIsStdlib(false) + } + builder + .clearNamedTypes() + .addAllNamedTypes(pkg.namedTypesList.map { named -> + named.toBuilder().setFullName(normalizer.symbol(named.fullName)).build() + }) + .clearFunctions() + .addAllFunctions(pkg.functionsList.map { function -> + function.toBuilder().setFullName(normalizer.symbol(function.fullName)).build() + }) + .clearGlobals() + .addAllGlobals(pkg.globalsList.map { global -> + global.toBuilder().setFullName(normalizer.symbol(global.fullName)).build() + }) + .clearConstants() + .addAllConstants(pkg.constantsList.map { constant -> + constant.toBuilder().setFullName(normalizer.symbol(constant.fullName)).build() + }) + .build() + } + return model.toBuilder().clearPackages().addAllPackages(packages).build() + } + + private fun mergeModelStructFields( + base: ProtoProgram, + model: ProtoProgram, + plans: Map, + remap: IdRemap, + counters: IdCounters, + source: String, + ): ProtoProgram { + var types = base.typesList + var packages = base.packagesList + val modelIndex = ProtoIndex(model) + + plans.toSortedMap().forEach { (modelNamedId, plan) -> + if (plan.extraFieldNames.isEmpty()) { + return@forEach + } + val remappedNamedId = remap.named.getValue(modelNamedId) + val modelNamed = modelIndex.named[remappedNamedId] + ?: throw IllegalArgumentException( + "merging Go model $source: model named type $remappedNamedId is missing", + ) + val extraFields = modelNamed.fieldsList.filter { it.name in plan.extraFieldNames } + val baseIndex = ProtoIndex( + base.toBuilder().clearTypes().addAllTypes(types).clearPackages().addAllPackages(packages).build(), + ) + val targetNamed = baseIndex.named[plan.targetNamedId] + ?: throw IllegalArgumentException( + "merging Go model $source: Go model target named type " + + "${plan.targetNamedId} is missing", + ) + val underlying = baseIndex.types[targetNamed.underlyingTypeId] + ?.takeIf { it.typeCase == ProtoTypeDefinition.TypeCase.STRUCT_TYPE } + ?.structType + ?: throw IllegalArgumentException( + "merging Go model $source: Go model target type " + + "${targetNamed.fullName} has no struct body", + ) + + val combinedFields = underlying.fieldsList.toMutableList() + combinedFields += extraFields.map(::fieldDeclarationToStructField) + val combined = ProtoTypeDefinition.newBuilder() + .setId(counters.nextType()) + .setStructType(ProtoStructType.newBuilder().addAllFields(combinedFields)) + .build() + types = types + combined + packages = packages.map { pkg -> + if (pkg.namedTypesList.none { it.id == targetNamed.id }) { + pkg + } else { + pkg.toBuilder() + .clearNamedTypes() + .addAllNamedTypes(pkg.namedTypesList.map { named -> + if (named.id != targetNamed.id) { + named + } else { + named.toBuilder() + .addAllFields(extraFields) + .setUnderlyingTypeId(combined.id) + .build() + } + }) + .build() + } + } + } + return base.toBuilder().clearTypes().addAllTypes(types).clearPackages().addAllPackages(packages).build() + } + + private fun mergePackages( + base: ProtoProgram, + model: ProtoProgram, + ownedPackages: Set, + initFunctions: Set, + ): ProtoProgram { + val packages = base.packagesList.toMutableList() + val packageIndices = packages.mapIndexed { index, pkg -> pkg.id to index }.toMap().toMutableMap() + + model.packagesList.forEach { modelPackage -> + val basePosition = packageIndices[modelPackage.id] + if (basePosition == null) { + val added = if (modelPackage.id in ownedPackages) { + modelPackage.toBuilder() + .clearFunctions() + .addAllFunctions( + modelPackage.functionsList + .filterNot { it.id in initFunctions } + .map(::asModelSupport), + ) + .build() + } else { + modelPackage + } + packageIndices[added.id] = packages.size + packages += added + return@forEach + } + + val basePackage = packages[basePosition] + val named = basePackage.namedTypesList.toMutableList() + val namedIndices = named.mapIndexed { index, value -> value.id to index }.toMap().toMutableMap() + modelPackage.namedTypesList.forEach { modelNamed -> + val position = namedIndices[modelNamed.id] + if (position == null) { + namedIndices[modelNamed.id] = named.size + named += modelNamed + } else { + val current = named[position] + named[position] = current.toBuilder() + .clearMethodIds() + .addAllMethodIds(appendUniqueIds(current.methodIdsList, modelNamed.methodIdsList)) + .clearPointerMethodIds() + .addAllPointerMethodIds( + appendUniqueIds( + current.pointerMethodIdsList, + modelNamed.pointerMethodIdsList, + ), + ) + .build() + } + } + + val functions = basePackage.functionsList.toMutableList() + val functionIndices = functions + .mapIndexed { index, value -> value.id to index } + .toMap() + .toMutableMap() + modelPackage.functionsList.forEach { modelFunction -> + if (modelFunction.id in initFunctions) { + return@forEach + } + val position = functionIndices[modelFunction.id] + if (position == null) { + functionIndices[modelFunction.id] = functions.size + functions += if (modelPackage.id in ownedPackages) { + asModelSupport(modelFunction) + } else { + modelFunction + } + } else if (modelPackage.id in ownedPackages) { + functions[position] = functions[position].toBuilder() + .clearAnonFunctionIds() + .addAllAnonFunctionIds(modelFunction.anonFunctionIdsList) + .build() + } + } + + packages[basePosition] = basePackage.toBuilder() + .clearImportIds() + .addAllImportIds(appendUniqueIds(basePackage.importIdsList, modelPackage.importIdsList)) + .clearNamedTypes() + .addAllNamedTypes(named) + .clearFunctions() + .addAllFunctions(functions) + .clearGlobals() + .addAllGlobals(mergeById(basePackage.globalsList, modelPackage.globalsList) { it.id }) + .clearConstants() + .addAllConstants(mergeById(basePackage.constantsList, modelPackage.constantsList) { it.id }) + .build() + } + + return base.toBuilder().clearPackages().addAllPackages(packages).build() + } + + private fun mergeFunctionBodies( + base: ProtoProgram, + model: ProtoProgram, + baseIndex: ProtoIndex, + ownedPackages: Set, + initFunctions: Set, + source: String, + ): ProtoProgram { + val bodies = base.functionBodiesList.toMutableList() + val bodyIndices = bodies.mapIndexed { index, body -> body.functionId to index }.toMap().toMutableMap() + val functionsWithBodies = mutableSetOf() + + model.functionBodiesList.forEach { body -> + if (body.functionId in initFunctions) { + return@forEach + } + val function = baseIndex.functions[body.functionId] ?: return@forEach + if (function.packageId !in ownedPackages) { + return@forEach + } + val previous = modeledFunctions.putIfAbsent(body.functionId, source) + require(previous == null) { + "Go function \"${function.fullName}\" is modeled more than once ($previous and $source)" + } + val position = bodyIndices[body.functionId] + if (position == null) { + bodyIndices[body.functionId] = bodies.size + bodies += body + } else { + bodies[position] = body + } + functionsWithBodies += body.functionId + } + + val packages = base.packagesList.map { pkg -> + if (pkg.functionsList.none { it.id in functionsWithBodies }) { + pkg + } else { + pkg.toBuilder() + .clearFunctions() + .addAllFunctions(pkg.functionsList.map { function -> + if (function.id in functionsWithBodies) { + function.toBuilder().setHasBody(true).build() + } else { + function + } + }) + .build() + } + } + return base.toBuilder() + .clearPackages() + .addAllPackages(packages) + .clearFunctionBodies() + .addAllFunctionBodies(bodies) + .build() + } + + private fun asModelSupport(function: ProtoFunction): ProtoFunction { + return function.toBuilder() + .setIsSynthetic(true) + .setSyntheticKind(MODEL_SUPPORT_KIND) + .build() + } +} + +private data class ModelPathNormalizer( + val replacements: Map, + val targets: Set, + private val orderedModelPaths: List, +) { + fun path(value: String): String = replacements[value] ?: value + + fun symbol(value: String): String { + return orderedModelPaths.fold(value) { result, modelPath -> + result.replace(modelPath, replacements.getValue(modelPath)) + } + } + + companion object { + fun create(program: ProtoProgram): ModelPathNormalizer { + val replacements = program.packagesList + .filterNot { it.isDependency || it.isStdlib || it.importPath == UNIVERSE_PACKAGE_PATH } + .associate { pkg -> + require(pkg.importPath.startsWith(GO_MODEL_PREFIX)) { + "Go model package \"${pkg.importPath}\" is not model-addressed, " + + "its import path must be ${GO_MODEL_PREFIX}" + } + val target = pkg.importPath.removePrefix(GO_MODEL_PREFIX) + require(target.isNotEmpty()) { + "Go model package \"${pkg.importPath}\" has an empty target package" + } + pkg.importPath to target + } + require(replacements.isNotEmpty()) { + "Go model contains no model-addressed packages, expected at least one package " + + "under $GO_MODEL_PREFIX" + } + return ModelPathNormalizer( + replacements, + replacements.values.toSet(), + replacements.keys.sortedByDescending(String::length), + ) + } + } +} + +private class ProtoIndex( + val program: ProtoProgram, + private val normalizer: ModelPathNormalizer? = null, +) { + val packages = program.packagesList.associateBy { it.id } + val types = program.typesList.associateBy { it.id } + val named = program.packagesList.flatMap { it.namedTypesList }.associateBy { it.id } + val functions = program.packagesList.flatMap { it.functionsList }.associateBy { it.id } + val globals = program.packagesList.flatMap { it.globalsList }.associateBy { it.id } + val consts = program.packagesList.flatMap { it.constantsList }.associateBy { it.id } + val packageKeys = program.packagesList.associate { it.id to path(it.importPath) } + val namedKeys = program.packagesList.flatMap { pkg -> + pkg.namedTypesList.map { named -> named.id to "${path(pkg.importPath)}.${named.name}" } + }.toMap() + + private val typeMemo = mutableMapOf() + private val activeTypes = mutableSetOf() + + fun typeKey(id: Int): String { + if (id == 0) { + return "void" + } + typeMemo[id]?.let { return it } + if (!activeTypes.add(id)) { + return "" + } + val definition = types[id] + val key = if (definition == null) { + "" + } else { + when (definition.typeCase) { + ProtoTypeDefinition.TypeCase.BASIC -> "basic:${definition.basic.kind.number}" + ProtoTypeDefinition.TypeCase.POINTER -> "pointer:${typeKey(definition.pointer.elemTypeId)}" + ProtoTypeDefinition.TypeCase.ARRAY -> + "array:${definition.array.length}:${typeKey(definition.array.elemTypeId)}" + ProtoTypeDefinition.TypeCase.SLICE -> "slice:${typeKey(definition.slice.elemTypeId)}" + ProtoTypeDefinition.TypeCase.MAP_TYPE -> + "map:${typeKey(definition.mapType.keyTypeId)}:" + + typeKey(definition.mapType.valueTypeId) + ProtoTypeDefinition.TypeCase.CHAN_TYPE -> + "chan:${definition.chanType.direction.number}:" + + typeKey(definition.chanType.elemTypeId) + ProtoTypeDefinition.TypeCase.STRUCT_TYPE -> buildString { + append("struct{") + definition.structType.fieldsList.forEach { field -> + append(field.name) + append(':') + append(typeKey(field.typeId)) + append(':') + append(field.embedded) + append(':') + append(field.exported) + append(':') + append(field.tag) + append('|') + } + append('}') + } + ProtoTypeDefinition.TypeCase.INTERFACE_TYPE -> buildString { + append("interface{") + definition.interfaceType.methodsList.forEach { method -> + append(method.name) + append(':') + append(typeKey(method.signatureTypeId)) + append('|') + } + definition.interfaceType.embedTypeIdsList.forEach { embedded -> + append("embed:") + append(typeKey(embedded)) + append('|') + } + append('}') + } + ProtoTypeDefinition.TypeCase.FUNC_TYPE -> buildString { + append("func(") + definition.funcType.paramTypeIdsList.forEach { parameter -> + append(typeKey(parameter)) + append(',') + } + append(")->(") + definition.funcType.resultTypeIdsList.forEach { result -> + append(typeKey(result)) + append(',') + } + append("):variadic=") + append(definition.funcType.variadic) + append(":recv=") + append(typeKey(definition.funcType.recvTypeId)) + } + ProtoTypeDefinition.TypeCase.NAMED_REF -> buildString { + append("named:") + append(namedKeys[definition.namedRef.namedTypeId]) + append('[') + definition.namedRef.typeArgIdsList.forEach { argument -> + append(typeKey(argument)) + append(',') + } + append(']') + } + ProtoTypeDefinition.TypeCase.TYPE_PARAM -> + "typeparam:${definition.typeParam.name}:${definition.typeParam.index}:" + + typeKey(definition.typeParam.constraintTypeId) + ProtoTypeDefinition.TypeCase.TUPLE -> buildString { + append("tuple(") + definition.tuple.elementTypeIdsList.forEach { element -> + append(typeKey(element)) + append(',') + } + append(')') + } + ProtoTypeDefinition.TypeCase.UNSAFE_POINTER -> "unsafe.Pointer" + ProtoTypeDefinition.TypeCase.TYPE_NOT_SET -> "" + } + } + activeTypes -= id + typeMemo[id] = key + return key + } + + fun reversePackageKeys(): MutableMap = packageKeys.reverse() + fun reverseNamedKeys(): MutableMap = namedKeys.reverse() + fun reverseTypeKeys(): MutableMap = typeKeysById().reverse() + fun reverseFunctionKeys(): MutableMap = functionKeysById().reverse() + fun reverseGlobalKeys(): MutableMap = globalKeysById().reverse() + fun reverseConstKeys(): MutableMap = constKeysById().reverse() + fun typeKeysById(): Map = types.keys.associateWith(::typeKey) + fun functionKeysById(): Map = functions.mapValues { symbol(it.value.fullName) } + fun globalKeysById(): Map = globals.mapValues { symbol(it.value.fullName) } + fun constKeysById(): Map = consts.mapValues { symbol(it.value.fullName) } + + fun isOwnedPackage(normalizer: ModelPathNormalizer, packageId: Int): Boolean { + val pkg = packages[packageId] ?: return false + return pkg.importPath in normalizer.replacements + } + + private fun path(value: String): String = normalizer?.path(value) ?: value + private fun symbol(value: String): String = normalizer?.symbol(value) ?: value +} + +private data class IdRemap( + val types: Map, + val packages: Map, + val functions: Map, + val named: Map, + val globals: Map, + val consts: Map, +) { + fun values(name: MapName): Map = when (name) { + MapName.TYPE -> types + MapName.PACKAGE -> packages + MapName.FUNCTION -> functions + MapName.NAMED -> named + MapName.GLOBAL -> globals + MapName.CONST -> consts + MapName.NONE -> emptyMap() + } +} + +private class IdCounters( + private var typeId: Int, + private var packageId: Int, + private var functionId: Int, + private var namedId: Int, + private var globalId: Int, + private var constId: Int, +) { + fun nextType(): Int = ++typeId + fun nextPackage(): Int = ++packageId + fun nextFunction(): Int = ++functionId + fun nextNamed(): Int = ++namedId + fun nextGlobal(): Int = ++globalId + fun nextConst(): Int = ++constId + + companion object { + fun forProgram(program: ProtoProgram): IdCounters = IdCounters( + typeId = program.typesList.maxOfOrNull { it.id } ?: 0, + packageId = program.packagesList.maxOfOrNull { it.id } ?: 0, + functionId = program.packagesList.flatMap { it.functionsList }.maxOfOrNull { it.id } ?: 0, + namedId = program.packagesList.flatMap { it.namedTypesList }.maxOfOrNull { it.id } ?: 0, + globalId = program.packagesList.flatMap { it.globalsList }.maxOfOrNull { it.id } ?: 0, + constId = program.packagesList.flatMap { it.constantsList }.maxOfOrNull { it.id } ?: 0, + ) + } +} + +private data class ModelStructFieldPlan( + val targetNamedId: Int, + val indexByModel: Map, + val extraFieldNames: Set, +) + +private fun planModelStructFields( + baseIndex: ProtoIndex, + modelIndex: ProtoIndex, + normalizer: ModelPathNormalizer, +): Map { + val plans = mutableMapOf() + val baseNamed = baseIndex.reverseNamedKeys() + modelIndex.program.packagesList.forEach { pkg -> + if (pkg.importPath !in normalizer.replacements) { + return@forEach + } + pkg.namedTypesList.forEach { modelNamed -> + val targetId = baseNamed[modelIndex.namedKeys.getValue(modelNamed.id)] ?: return@forEach + val targetNamed = baseIndex.named.getValue(targetId) + if (modelNamed.kind != ProtoNamedTypeKind.NAMED_TYPE_STRUCT) { + return@forEach + } + require(targetNamed.kind == ProtoNamedTypeKind.NAMED_TYPE_STRUCT) { + "Go model type ${modelIndex.namedKeys.getValue(modelNamed.id)} is a struct, " + + "but its target is not a struct" + } + val targetFields = targetNamed.fieldsList.associateBy { it.name } + var nextIndex = targetNamed.fieldsList.maxOfOrNull { it.index + 1 } ?: 0 + val indexByModel = mutableMapOf() + val extraFieldNames = mutableSetOf() + modelNamed.fieldsList.forEach { modelField -> + val targetField = targetFields[modelField.name] + if (targetField == null) { + indexByModel[modelField.index] = nextIndex++ + extraFieldNames += modelField.name + } else { + require( + modelIndex.typeKey(modelField.typeId) == baseIndex.typeKey(targetField.typeId), + ) { + "Go model field ${modelIndex.namedKeys.getValue(modelNamed.id)}." + + "${modelField.name} has a different type from its target" + } + indexByModel[modelField.index] = targetField.index + } + } + plans[modelNamed.id] = ModelStructFieldPlan(targetId, indexByModel, extraFieldNames) + } + } + return plans +} + +private fun applyModelStructFieldPlans( + model: ProtoProgram, + plans: Map, +): ProtoProgram { + val underlyingPlans = model.packagesList + .flatMap { it.namedTypesList } + .mapNotNull { named -> plans[named.id]?.let { named.underlyingTypeId to it } } + .toMap() + val types = model.typesList.map { type -> + val plan = underlyingPlans[type.id] + if (plan == null || type.typeCase != ProtoTypeDefinition.TypeCase.STRUCT_TYPE) { + type + } else { + val fields = type.structType.fieldsList.map { field -> + val index = plan.indexByModel[field.index] ?: field.index + field.toBuilder().setIndex(index).build() + } + type.toBuilder().setStructType(type.structType.toBuilder().clearFields().addAllFields(fields)).build() + } + } + val packages = model.packagesList.map { pkg -> + pkg.toBuilder() + .clearNamedTypes() + .addAllNamedTypes(pkg.namedTypesList.map { named -> + val plan = plans[named.id] ?: return@map named + named.toBuilder() + .clearFields() + .addAllFields(named.fieldsList.map { field -> + field.toBuilder() + .setIndex(plan.indexByModel.getValue(field.index)) + .build() + }) + .build() + }) + .build() + } + return model.toBuilder().clearTypes().addAllTypes(types).clearPackages().addAllPackages(packages).build() +} + +private fun remapModelStructFieldAccesses( + model: ProtoProgram, + modelIndex: ProtoIndex, + plans: Map, +): ProtoProgram { + val underlyingToNamed = modelIndex.named.values.associate { it.underlyingTypeId to it.id } + + fun remap(typeId: Int, fieldIndex: Int, fieldName: String): Int { + val namedId = modelNamedTypeForFieldAccess(modelIndex, underlyingToNamed, typeId, mutableSetOf()) + val plan = plans[namedId] ?: return fieldIndex + return plan.indexByModel[fieldIndex] + ?: throw IllegalArgumentException( + "Go model field access $fieldName[$fieldIndex] has no target field mapping", + ) + } + + val bodies = model.functionBodiesList.map { body -> + body.toBuilder() + .clearBlocks() + .addAllBlocks(body.blocksList.map { block -> + block.toBuilder() + .clearInstructions() + .addAllInstructions(block.instructionsList.map { instruction -> + when (instruction.instCase) { + org.opentaint.ir.go.proto.ProtoInstruction.InstCase.FIELD_ADDR -> { + val access = instruction.fieldAddr + instruction.toBuilder().setFieldAddr( + access.toBuilder().setFieldIndex( + remap(access.x.typeId, access.fieldIndex, access.fieldName), + ), + ).build() + } + org.opentaint.ir.go.proto.ProtoInstruction.InstCase.FIELD -> { + val access = instruction.field + instruction.toBuilder().setField( + access.toBuilder().setFieldIndex( + remap(access.x.typeId, access.fieldIndex, access.fieldName), + ), + ).build() + } + else -> instruction + } + }) + .build() + }) + .build() + } + return model.toBuilder().clearFunctionBodies().addAllFunctionBodies(bodies).build() +} + +private fun modelNamedTypeForFieldAccess( + index: ProtoIndex, + underlyingToNamed: Map, + typeId: Int, + active: MutableSet, +): Int? { + if (typeId == 0 || !active.add(typeId)) { + return null + } + val type = index.types[typeId] ?: return null + val result = when (type.typeCase) { + ProtoTypeDefinition.TypeCase.POINTER -> + modelNamedTypeForFieldAccess(index, underlyingToNamed, type.pointer.elemTypeId, active) + ProtoTypeDefinition.TypeCase.NAMED_REF -> type.namedRef.namedTypeId + ProtoTypeDefinition.TypeCase.STRUCT_TYPE -> underlyingToNamed[typeId] + else -> null + } + active -= typeId + return result +} + +private fun fieldDeclarationToStructField(field: ProtoFieldDecl): ProtoStructField { + return ProtoStructField.newBuilder() + .setName(field.name) + .setTypeId(field.typeId) + .setIndex(field.index) + .setEmbedded(field.embedded) + .setExported(field.exported) + .setTag(field.tag) + .build() +} + +private fun assignIds( + source: Map, + existing: MutableMap, + next: () -> Int, +): Map { + return buildMap { + source.toSortedMap().forEach { (oldId, key) -> + put(oldId, existing[key] ?: next().also { existing[key] = it }) + } + } +} + +private enum class MapName { + NONE, + TYPE, + PACKAGE, + FUNCTION, + NAMED, + GLOBAL, + CONST, +} + +private fun remapMessageIds(message: Message, remap: IdRemap): Message { + val builder = message.toBuilder() + message.allFields.forEach { (field, value) -> + if (field.isRepeated) { + val values = value as List<*> + if (field.javaType == FieldDescriptor.JavaType.MESSAGE) { + builder.clearField(field) + values.forEach { child -> + builder.addRepeatedField(field, remapMessageIds(child as Message, remap)) + } + } else { + val mapName = remapClass(message, field) + if (mapName != MapName.NONE) { + values.forEachIndexed { index, oldId -> + builder.setRepeatedField( + field, + index, + remapValue(oldId as Int, remap.values(mapName), message, field), + ) + } + } + } + } else if (field.javaType == FieldDescriptor.JavaType.MESSAGE) { + builder.setField(field, remapMessageIds(value as Message, remap)) + } else { + val mapName = remapClass(message, field) + if (mapName != MapName.NONE) { + builder.setField( + field, + remapValue(value as Int, remap.values(mapName), message, field), + ) + } + } + } + return builder.build() +} + +private fun remapValue( + oldId: Int, + values: Map, + message: Message, + field: FieldDescriptor, +): Int { + if (oldId == 0) { + return 0 + } + return values[oldId] ?: throw IllegalArgumentException( + "remapping ${message.descriptorForType.name}.${field.name}: " + + "wire id $oldId has no model remapping", + ) +} + +private fun remapClass(message: Message, field: FieldDescriptor): MapName { + val name = field.name + if (name == "id") { + return when (message.descriptorForType.name) { + "ProtoTypeDefinition" -> MapName.TYPE + "ProtoPackage" -> MapName.PACKAGE + "ProtoNamedType" -> MapName.NAMED + "ProtoFunction" -> MapName.FUNCTION + "ProtoGlobal" -> MapName.GLOBAL + "ProtoConst" -> MapName.CONST + else -> MapName.NONE + } + } + return when { + name == "named_type_id" || name == "embedded_interface_ids" -> MapName.NAMED + name == "package_id" || name == "import_ids" -> MapName.PACKAGE + name == "fn_id" || name == "method_ids" || name == "pointer_method_ids" || + "function_id" in name -> MapName.FUNCTION + name == "global_id" -> MapName.GLOBAL + name.endsWith("type_id") || name.endsWith("type_ids") -> MapName.TYPE + else -> MapName.NONE + } +} + +private fun appendUniqueIds(base: List, added: List): List { + val result = base.toMutableList() + val seen = base.toMutableSet() + added.forEach { value -> + if (value != 0 && seen.add(value)) { + result += value + } + } + return result +} + +private fun mergeById(base: List, added: List, id: (T) -> Int): List { + val result = base.toMutableList() + val seen = base.mapTo(mutableSetOf(), id) + added.forEach { value -> + if (seen.add(id(value))) { + result += value + } + } + return result +} + +private fun Map.reverse(): MutableMap { + return entries.associateTo(mutableMapOf()) { (key, value) -> value to key } +} + +private const val GO_MODEL_PREFIX = "opentaint/" +private const val UNIVERSE_PACKAGE_PATH = "" +private const val MODEL_SUPPORT_KIND = "opentaint model support" diff --git a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt index 0e7fdec906..99fff2d3eb 100644 --- a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt +++ b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt @@ -1,15 +1,22 @@ package org.opentaint.ir.go.test.features import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import org.opentaint.ir.go.cfg.GoIRCallTarget import org.opentaint.ir.go.client.GoIRLoadConfig import org.opentaint.ir.go.client.GoIRLoadMode import org.opentaint.ir.go.expr.GoIRFieldAddrExpr import org.opentaint.ir.go.ext.findExpressions +import org.opentaint.ir.go.ext.findInstructions +import org.opentaint.ir.go.inst.GoIRCall +import org.opentaint.ir.go.inst.GoIRGlobalStore import org.opentaint.ir.go.inst.GoIRReturn import org.opentaint.ir.go.test.GoIRTestBuilder import org.opentaint.ir.go.test.GoIRTestExtension +import org.opentaint.ir.go.value.GoIRConstValue +import org.opentaint.ir.go.value.GoIRConstantValue import org.opentaint.ir.go.value.GoIRParameterValue import java.nio.file.Files import java.nio.file.Path @@ -91,6 +98,264 @@ class ModelTests { assertThat(box.pointerMethods.single { it.name == "Keep" }.bodyAvailable).isTrue() } + @Test + fun `model package name must match target package name`(builder: GoIRTestBuilder) { + val project = project( + "example.com/app", + "package main\nimport \"strings\"\nfunc Use(value string) string { return strings.ToUpper(value) }\n", + ) + val model = model( + "strings", + "package modelstrings\nfunc ToUpper(value string) string { return value }\n", + ) + + assertThatThrownBy { + builder.buildFromDir(project, modelConfig(model)) + }.isInstanceOf(IllegalArgumentException::class.java) + .hasMessageContaining("package name \"modelstrings\", want \"strings\"") + } + + @Test + fun `model function signature must match target function`(builder: GoIRTestBuilder) { + val project = project( + "example.com/app", + "package main\nimport \"strings\"\nfunc Use(value string) string { return strings.ToUpper(value) }\n", + ) + val model = model( + "strings", + "package strings\nfunc ToUpper(value int) int { return value }\n", + ) + + assertThatThrownBy { + builder.buildFromDir(project, modelConfig(model)) + }.isInstanceOf(IllegalArgumentException::class.java) + .hasMessageContaining("strings.ToUpper has a different signature") + } + + @Test + fun `one package cannot be modeled twice`(builder: GoIRTestBuilder) { + val project = project( + "example.com/app", + "package main\nimport \"strings\"\nfunc Use(value string) string { return strings.ToUpper(value) }\n", + ) + val source = "package strings\nfunc ToUpper(value string) string { return value }\n" + val firstModel = model("strings", source) + val secondModel = model("strings", source) + + assertThatThrownBy { + builder.buildFromDir( + project, + GoIRLoadConfig( + mode = GoIRLoadMode.PROJECT, + modelDirs = listOf(firstModel, secondModel), + ), + ) + }.isInstanceOf(IllegalArgumentException::class.java) + .hasMessageContaining("Go package \"strings\" is modeled more than once") + } + + @Test + fun `model package requires opentaint address prefix`(builder: GoIRTestBuilder) { + val project = project( + "example.com/app", + "package main\nimport \"strings\"\nfunc Use(value string) string { return strings.ToUpper(value) }\n", + ) + val model = model( + targetPath = "strings", + source = "package strings\nfunc ToUpper(value string) string { return value }\n", + module = "models", + ) + + assertThatThrownBy { + builder.buildFromDir(project, modelConfig(model)) + }.isInstanceOf(IllegalArgumentException::class.java) + .hasMessageContaining("import path must be opentaint/") + } + + @Test + fun `model initializer and its closures do not replace target initialization`(builder: GoIRTestBuilder) { + val project = project( + "example.com/initmodel", + "package initmodel\nvar Value string\nfunc init() { Value = \"original\" }\n", + ) + val model = model( + "example.com/initmodel", + "package initmodel\nvar Value string\nfunc init() { write := func() { Value = \"model\" }; write() }\n", + ) + + val program = builder.buildFromDir(project, modelConfig(model)) + val target = program.findPackage("example.com/initmodel")!! + val initializer = target.functions.single { it.name == "init#1" } + val values = initializer.findInstructions() + .mapNotNull { (it.value as? GoIRConstValue)?.value as? GoIRConstantValue.StringConst } + .map { it.value } + + assertThat(values).contains("original").doesNotContain("model") + assertThat(target.functions.map { it.fullName }).noneMatch { "opentaint" in it } + } + + @Test + fun `existing model field type must match target field type`(builder: GoIRTestBuilder) { + val project = project( + "example.com/fieldtype", + "package fieldtype\ntype Box struct { Value string }\nfunc (b *Box) Put(value string) {}\n", + ) + val model = model( + "example.com/fieldtype", + "package fieldtype\ntype Box struct { Value int }\nfunc (b *Box) Put(value string) { b.Value = len(value) }\n", + ) + + assertThatThrownBy { + builder.buildFromDir(project, modelConfig(model)) + }.isInstanceOf(IllegalArgumentException::class.java) + .hasMessageContaining("field example.com/fieldtype.Box.Value has a different type") + } + + @Test + fun `versioned target path can use its declared package name`(builder: GoIRTestBuilder) { + val project = project( + "github.com/acme/widget/v2", + "package widget\nfunc Identity(value string) string { return \"original\" }\n", + ) + val model = model( + "github.com/acme/widget/v2", + "package widget\nfunc Identity(value string) string { return value }\n", + ) + + val program = builder.buildFromDir(project, modelConfig(model)) + val identity = program.findPackage("github.com/acme/widget/v2")!! + .functions.single { it.name == "Identity" } + val returned = (identity.body!!.blocks.single().terminator as GoIRReturn).results.single() + + assertThat(returned).isInstanceOf(GoIRParameterValue::class.java) + } + + @Test + fun `model can use the dependency that it models`(builder: GoIRTestBuilder) { + val project = project( + "example.com/dependency", + "package dependency\ntype Value struct { Text string }\n" + + "func Identity(value Value) Value { return Value{Text: \"original\"} }\n", + ) + val model = model( + "example.com/dependency", + "package dependency\nimport target \"example.com/dependency\"\n" + + "func Identity(value target.Value) target.Value { return value }\n", + ) + + val program = builder.buildFromDir(project, modelConfig(model)) + val identity = program.findPackage("example.com/dependency")!! + .functions.single { it.name == "Identity" && it.parent == null } + val returned = (identity.body!!.blocks.single().terminator as GoIRReturn).results.single() + + assertThat((returned as GoIRParameterValue).paramIndex).isZero() + } + + @Test + fun `model adds support declarations and keeps helper calls`(builder: GoIRTestBuilder) { + val project = project( + "example.com/support", + "package support\nfunc Target(value string) string { return \"original\" }\n", + ) + val model = model( + "example.com/support", + """ + package support + type Added struct { Text string } + var AddedGlobal string + const AddedConstant = "constant" + func helper(value string) string { return value } + func Target(value string) string { return helper(value) } + """.trimIndent(), + ) + + val program = builder.buildFromDir(project, modelConfig(model)) + val targetPackage = program.findPackage("example.com/support")!! + val helper = targetPackage.functions.single { it.name == "helper" } + val target = targetPackage.functions.single { it.name == "Target" } + val calledFunction = target.findInstructions().single().call.target as GoIRCallTarget.Function + + assertThat(targetPackage.findNamedType("Added")).isNotNull() + assertThat(targetPackage.findGlobal("AddedGlobal")).isNotNull() + assertThat(targetPackage.findConstant("AddedConstant")).isNotNull() + assertThat(helper.isSynthetic).isTrue() + assertThat(helper.syntheticKind).isEqualTo("opentaint model support") + assertThat(helper.bodyAvailable).isTrue() + assertThat(calledFunction.function).isSameAs(helper) + } + + @Test + fun `model body owns its anonymous functions`(builder: GoIRTestBuilder) { + val project = project( + "example.com/closures", + "package closures\nfunc Apply(value string) string { return \"original\" }\n", + ) + val model = model( + "example.com/closures", + "package closures\nfunc Apply(value string) string { " + + "nested := func(input string) string { return input }; return nested(value) }\n", + ) + + val program = builder.buildFromDir(project, modelConfig(model)) + val apply = program.findPackage("example.com/closures")!!.functions.single { it.name == "Apply" } + val closure = apply.anonymousFunctions.single().function + + assertThat(apply.anonymousFunctions).hasSize(1) + assertThat(closure.parent!!.function).isSameAs(apply) + assertThat(closure.bodyAvailable).isTrue() + } + + @Test + fun `one model module can model multiple target packages`(builder: GoIRTestBuilder) { + val project = Files.createTempDirectory("goir-multi-package-project") + project.resolve("go.mod").write("module example.com/multiple\ngo 1.22\n") + listOf("first", "second").forEach { name -> + val packageDir = project.resolve(name) + Files.createDirectories(packageDir) + packageDir.resolve("source.go").write( + "package $name\nfunc Identity(value string) string { return \"original\" }\n", + ) + } + + val model = Files.createTempDirectory("goir-multi-package-model") + model.resolve("go.mod").write("module opentaint\ngo 1.25\n") + listOf("first", "second").forEach { name -> + val packageDir = model.resolve("example.com/multiple/$name") + Files.createDirectories(packageDir) + packageDir.resolve("model.go").write( + "package $name\nfunc Identity(value string) string { return value }\n", + ) + } + + val program = builder.buildFromDir(project, modelConfig(model)) + listOf("first", "second").forEach { name -> + val identity = program.findPackage("example.com/multiple/$name")!! + .functions.single { it.name == "Identity" } + val returned = (identity.body!!.blocks.single().terminator as GoIRReturn).results.single() + assertThat(returned).isInstanceOf(GoIRParameterValue::class.java) + } + } + + private fun project(module: String, source: String): Path { + val project = Files.createTempDirectory("goir-model-project") + project.resolve("go.mod").write("module $module\ngo 1.22\n") + project.resolve("source.go").write(source) + return project + } + + private fun model(targetPath: String, source: String, module: String = "opentaint"): Path { + val model = Files.createTempDirectory("goir-model") + model.resolve("go.mod").write("module $module\ngo 1.25\n") + val modelPackage = model.resolve(targetPath) + Files.createDirectories(modelPackage) + modelPackage.resolve("model.go").write(source) + return model + } + + private fun modelConfig(model: Path): GoIRLoadConfig { + return GoIRLoadConfig(mode = GoIRLoadMode.PROJECT, modelDirs = listOf(model)) + } + private fun Path.write(content: String) { toFile().writeText(content) } diff --git a/core/opentaint-ir/go/go-ssa-server/build_program_test.go b/core/opentaint-ir/go/go-ssa-server/build_program_test.go index 19babbda56..05d0bec7b6 100644 --- a/core/opentaint-ir/go/go-ssa-server/build_program_test.go +++ b/core/opentaint-ir/go/go-ssa-server/build_program_test.go @@ -16,99 +16,109 @@ import ( pb "github.com/opentaint/go-ir/go-ssa-server/proto/goir" ) +type buildResult struct { + program *pb.ProtoProgram + models []*pb.ProtoModelProgram +} + func writeTinyModule(t *testing.T) string { + t.Helper() dir := t.TempDir() - if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/m\ngo 1.22\n"), 0o644); err != nil { + writeFile(t, filepath.Join(dir, "go.mod"), "module example.com/m\ngo 1.22\n") + writeFile( + t, + filepath.Join(dir, "main.go"), + "package main\nimport \"strings\"\nfunc Greet(s string) string { return strings.ToUpper(s) }\nfunc main() { println(Greet(\"x\")) }\n", + ) + return dir +} + +func writeStringsModel(t *testing.T, source string) string { + t.Helper() + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "go.mod"), "module opentaint\ngo 1.25\n") + packageDir := filepath.Join(dir, "strings") + if err := os.MkdirAll(packageDir, 0o755); err != nil { t.Fatal(err) } - src := "package main\nimport \"strings\"\nfunc Greet(s string) string { return strings.ToUpper(s) }\nfunc main() { println(Greet(\"x\")) }\n" - if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644); err != nil { + writeFile(t, filepath.Join(packageDir, "model.go"), source) + return dir +} + +func writeFile(t *testing.T, path, content string) { + t.Helper() + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } - return dir } -func runBuildProgram(t *testing.T, req *pb.BuildProgramRequest) *pb.ProtoProgram { +func startBuild(t *testing.T, req *pb.BuildProgramRequest) pb.GoSSAService_BuildProgramClient { + t.Helper() lis := bufconn.Listen(1 << 20) srv := grpc.NewServer() pb.RegisterGoSSAServiceServer(srv, NewGoSSAServer("test", "test")) go srv.Serve(lis) - defer srv.Stop() + t.Cleanup(srv.Stop) - conn, err := grpc.NewClient("passthrough:///bufnet", - grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) }), + conn, err := grpc.NewClient( + "passthrough:///bufnet", + grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { + return lis.DialContext(ctx) + }), grpc.WithTransportCredentials(insecure.NewCredentials()), - // The bulk program payload can exceed gRPC's default 4 MB receive limit; - // mirror the production client (GoSsaServerProcess) which allows 256 MB. - grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(256*1024*1024))) + grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(256*1024*1024)), + ) if err != nil { t.Fatal(err) } - defer conn.Close() + t.Cleanup(func() { _ = conn.Close() }) stream, err := pb.NewGoSSAServiceClient(conn).BuildProgram(context.Background(), req) if err != nil { t.Fatal(err) } - program := &pb.ProtoProgram{} + return stream +} + +func runBuildResult(t *testing.T, req *pb.BuildProgramRequest) buildResult { + t.Helper() + stream := startBuild(t, req) + result := buildResult{program: &pb.ProtoProgram{}} for { resp, err := stream.Recv() + if err == io.EOF { + return result + } if err != nil { - if err != io.EOF { - t.Fatalf("stream error: %v", err) - } - break + t.Fatalf("stream error: %v", err) } - switch p := resp.Payload.(type) { + switch payload := resp.Payload.(type) { case *pb.BuildProgramResponse_Program: - program = p.Program + result.program = payload.Program + case *pb.BuildProgramResponse_ModelProgram: + result.models = append(result.models, payload.ModelProgram) case *pb.BuildProgramResponse_PackageDef: - program.Packages = append(program.Packages, p.PackageDef) + result.program.Packages = append(result.program.Packages, payload.PackageDef) case *pb.BuildProgramResponse_FunctionBody: - program.FunctionBodies = append(program.FunctionBodies, p.FunctionBody) + result.program.FunctionBodies = append(result.program.FunctionBodies, payload.FunctionBody) case *pb.BuildProgramResponse_Error: - if p.Error.Fatal { - t.Fatalf("fatal: %s", p.Error.Message) + if payload.Error.Fatal { + t.Fatalf("fatal: %s", payload.Error.Message) } } } - return program -} - -func runBuild(t *testing.T, req *pb.BuildProgramRequest) ([]*pb.ProtoPackage, map[int32]bool) { - program := runBuildProgram(t, req) - bodies := make(map[int32]bool, len(program.FunctionBodies)) - for _, body := range program.FunctionBodies { - bodies[body.FunctionId] = true - } - return program.Packages, bodies } func runBuildFatal(t *testing.T, req *pb.BuildProgramRequest) string { t.Helper() - lis := bufconn.Listen(1 << 20) - srv := grpc.NewServer() - pb.RegisterGoSSAServiceServer(srv, NewGoSSAServer("test", "test")) - go srv.Serve(lis) - defer srv.Stop() - conn, err := grpc.NewClient("passthrough:///bufnet", - grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) }), - grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - stream, err := pb.NewGoSSAServiceClient(conn).BuildProgram(context.Background(), req) - if err != nil { - t.Fatal(err) - } + stream := startBuild(t, req) for { - resp, recvErr := stream.Recv() - if recvErr == io.EOF { - t.Fatal("expected a fatal model validation error") + resp, err := stream.Recv() + if err == io.EOF { + t.Fatal("expected a fatal Go model build error") } - if recvErr != nil { - t.Fatalf("expected a model validation error, got stream error: %v", recvErr) + if err != nil { + t.Fatalf("expected a Go model build error, got stream error: %v", err) } if failure, ok := resp.Payload.(*pb.BuildProgramResponse_Error); ok && failure.Error.Fatal { return failure.Error.Message @@ -117,19 +127,19 @@ func runBuildFatal(t *testing.T, req *pb.BuildProgramRequest) string { } func findPkg(pkgs []*pb.ProtoPackage, path string) *pb.ProtoPackage { - for _, p := range pkgs { - if p.ImportPath == path { - return p + for _, pkg := range pkgs { + if pkg.ImportPath == path { + return pkg } } return nil } -func bodyIDsOf(p *pb.ProtoPackage, bodies map[int32]bool) (withBody, total int) { - for _, f := range p.Functions { - if f.HasBody { +func bodyIDsOf(pkg *pb.ProtoPackage, bodies map[int32]bool) (withBody, total int) { + for _, function := range pkg.Functions { + if function.HasBody { total++ - if bodies[f.Id] { + if bodies[function.Id] { withBody++ } } @@ -138,240 +148,97 @@ func bodyIDsOf(p *pb.ProtoPackage, bodies map[int32]bool) (withBody, total int) } func TestBuildProgram_ProjectMode_NoStdlibBodies(t *testing.T) { - dir := writeTinyModule(t) - pkgs, bodies := runBuild(t, &pb.BuildProgramRequest{WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, InstantiateGenerics: true, SanityCheck: true}) - - main := findPkg(pkgs, "example.com/m") - if main == nil || main.IsDependency || main.IsStdlib { - t.Fatalf("main package missing or misclassified: %+v", main) - } - wb, total := bodyIDsOf(main, bodies) - if total == 0 || wb != total { - t.Fatalf("project bodies: want all %d, got %d", total, wb) - } - strs := findPkg(pkgs, "strings") - if strs == nil || !strs.IsStdlib { - t.Fatalf("strings not present/stdlib: %+v", strs) - } - if wb, _ := bodyIDsOf(strs, bodies); wb != 0 { - t.Fatalf("PROJECT mode streamed %d stdlib bodies, want 0", wb) + result := runBuildResult(t, &pb.BuildProgramRequest{ + WorkingDir: writeTinyModule(t), Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + InstantiateGenerics: true, SanityCheck: true, + }) + bodies := make(map[int32]bool, len(result.program.FunctionBodies)) + for _, body := range result.program.FunctionBodies { + bodies[body.FunctionId] = true } -} -func TestBuildProgram_FullMode_StreamsStdlibBodies(t *testing.T) { - dir := writeTinyModule(t) - pkgs, bodies := runBuild(t, &pb.BuildProgramRequest{WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_FULL, InstantiateGenerics: true, SanityCheck: true}) - strs := findPkg(pkgs, "strings") - if strs == nil { - t.Fatal("strings package missing in FULL mode") + mainPackage := findPkg(result.program.Packages, "example.com/m") + if mainPackage == nil || mainPackage.IsDependency || mainPackage.IsStdlib { + t.Fatalf("main package missing or misclassified: %+v", mainPackage) } - if !strs.IsStdlib { - t.Fatalf("strings not stdlib: %+v", strs) + withBody, total := bodyIDsOf(mainPackage, bodies) + if total == 0 || withBody != total { + t.Fatalf("project bodies: want all %d, got %d", total, withBody) } - if wb, _ := bodyIDsOf(strs, bodies); wb == 0 { - t.Fatal("FULL mode streamed no stdlib bodies") - } -} - -func writeStringsModel(t *testing.T, packageName string) string { - dir := t.TempDir() - if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { - t.Fatal(err) - } - packageDir := filepath.Join(dir, "strings") - if err := os.MkdirAll(packageDir, 0o755); err != nil { - t.Fatal(err) + stringsPackage := findPkg(result.program.Packages, "strings") + if stringsPackage == nil || !stringsPackage.IsStdlib { + t.Fatalf("strings not present or not standard library: %+v", stringsPackage) } - src := "package " + packageName + "\nfunc ToUpper(s string) string { return s }\n" - if err := os.WriteFile(filepath.Join(packageDir, "model.go"), []byte(src), 0o644); err != nil { - t.Fatal(err) + if withBody, _ := bodyIDsOf(stringsPackage, bodies); withBody != 0 { + t.Fatalf("PROJECT mode streamed %d standard library bodies, want 0", withBody) } - return dir } -func TestBuildProgram_GoModelReplacesTargetBody(t *testing.T) { - dir := writeTinyModule(t) - modelDir := writeStringsModel(t, "strings") - program := runBuildProgram(t, &pb.BuildProgramRequest{ - WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, - InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, +func TestBuildProgram_FullMode_StreamsStdlibBodies(t *testing.T) { + result := runBuildResult(t, &pb.BuildProgramRequest{ + WorkingDir: writeTinyModule(t), Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_FULL, + InstantiateGenerics: true, SanityCheck: true, }) - - if findPkg(program.Packages, "opentaint/strings") != nil { - t.Fatal("model-addressed package leaked into the merged program") - } - stringsPkg := findPkg(program.Packages, "strings") - if stringsPkg == nil { - t.Fatal("strings package missing") - } - var target *pb.ProtoFunction - for _, fn := range stringsPkg.Functions { - if fn.Name == "ToUpper" { - target = fn - break - } - } - if target == nil { - t.Fatal("strings.ToUpper missing") - } - var body *pb.ProtoFunctionBody - for _, candidate := range program.FunctionBodies { - if candidate.FunctionId == target.Id { - body = candidate - break - } - } - if body == nil { - t.Fatal("model body was not attached to strings.ToUpper") + bodies := make(map[int32]bool, len(result.program.FunctionBodies)) + for _, body := range result.program.FunctionBodies { + bodies[body.FunctionId] = true } - if len(body.Blocks) != 1 || len(body.Blocks[0].Instructions) == 0 { - t.Fatalf("unexpected model body: %+v", body) + stringsPackage := findPkg(result.program.Packages, "strings") + if stringsPackage == nil || !stringsPackage.IsStdlib { + t.Fatalf("strings package is missing or not standard library: %+v", stringsPackage) } - ret := body.Blocks[0].Instructions[len(body.Blocks[0].Instructions)-1].GetReturnInst() - if ret == nil || len(ret.Results) != 1 || ret.Results[0].GetParamIndex() != 0 { - t.Fatalf("strings.ToUpper model does not return its parameter: %+v", ret) + if withBody, _ := bodyIDsOf(stringsPackage, bodies); withBody == 0 { + t.Fatal("FULL mode streamed no standard library bodies") } } -func TestBuildProgram_GoModelPackageNameMustMatchTarget(t *testing.T) { - dir := writeTinyModule(t) - modelDir := writeStringsModel(t, "modelstrings") - message := runBuildFatal(t, &pb.BuildProgramRequest{ - WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, +func TestBuildProgram_StreamsRawGoModelForKotlinResolution(t *testing.T) { + modelDir := writeStringsModel( + t, + "package strings\nfunc ToUpper(value string) string { return value }\n", + ) + result := runBuildResult(t, &pb.BuildProgramRequest{ + WorkingDir: writeTinyModule(t), Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, }) - if !strings.Contains(message, "package name \"modelstrings\", want \"strings\"") { - t.Fatalf("unexpected validation error: %s", message) - } -} - -func TestBuildProgram_GoModelFunctionSignatureMustMatchTarget(t *testing.T) { - dir := writeTinyModule(t) - modelDir := writeStringsModel(t, "strings") - modelSource := "package strings\nfunc ToUpper(value int) int { return value }\n" - if err := os.WriteFile(filepath.Join(modelDir, "strings", "model.go"), []byte(modelSource), 0o644); err != nil { - t.Fatal(err) - } - - message := runBuildFatal(t, &pb.BuildProgramRequest{ - WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, - InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, - }) - if !strings.Contains(message, "strings.ToUpper has a different signature") { - t.Fatalf("unexpected validation error: %s", message) - } -} - -func TestBuildProgram_GoPackageCannotBeModeledTwice(t *testing.T) { - dir := writeTinyModule(t) - firstModel := writeStringsModel(t, "strings") - secondModel := writeStringsModel(t, "strings") - - message := runBuildFatal(t, &pb.BuildProgramRequest{ - WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, - InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{firstModel, secondModel}, - }) - if !strings.Contains(message, "Go package \"strings\" is modeled more than once") { - t.Fatalf("unexpected validation error: %s", message) - } -} -func TestBuildProgram_GoModelDoesNotReplacePackageInitializer(t *testing.T) { - projectDir := t.TempDir() - if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte("module example.com/initmodel\ngo 1.22\n"), 0o644); err != nil { - t.Fatal(err) + if len(result.models) != 1 { + t.Fatalf("want one raw model program, got %d", len(result.models)) } - projectSource := "package initmodel\nvar Value string\nfunc init() { Value = \"original\" }\n" - if err := os.WriteFile(filepath.Join(projectDir, "init.go"), []byte(projectSource), 0o644); err != nil { - t.Fatal(err) + model := result.models[0] + if model.Source != modelDir { + t.Fatalf("model source is %q, want %q", model.Source, modelDir) } - - modelDir := t.TempDir() - if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { - t.Fatal(err) + if findPkg(model.Program.Packages, "opentaint/strings") == nil { + t.Fatal("raw model program has no opentaint/strings package") } - modelPackageDir := filepath.Join(modelDir, "example.com", "initmodel") - if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { - t.Fatal(err) + if findPkg(result.program.Packages, "opentaint/strings") != nil { + t.Fatal("base program contains the raw model package") } - modelSource := "package initmodel\nvar Value string\nfunc init() { Value = \"model\" }\n" - if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { - t.Fatal(err) - } - - program := runBuildProgram(t, &pb.BuildProgramRequest{ - WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, - InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, - }) - targetPackage := findPkg(program.Packages, "example.com/initmodel") - if targetPackage == nil || targetPackage.InitFunctionId == 0 { - t.Fatalf("target package initializer missing: %+v", targetPackage) + stringsPackage := findPkg(result.program.Packages, "strings") + if stringsPackage == nil { + t.Fatal("base program has no strings package") } - var sourceInitializerID int32 - for _, function := range targetPackage.Functions { - if function.Name == "init#1" { - sourceInitializerID = function.Id - break + var toUpperID int32 + for _, function := range stringsPackage.Functions { + if function.Name == "ToUpper" { + toUpperID = function.Id } } - if sourceInitializerID == 0 { - t.Fatalf("target source initializer missing: %+v", targetPackage) - } - for _, body := range program.FunctionBodies { - if body.FunctionId != sourceInitializerID { - continue - } - var sawOriginal, sawModel bool - for _, block := range body.Blocks { - for _, instruction := range block.Instructions { - store := instruction.GetStore() - if store == nil || store.Val.GetConstVal() == nil { - continue - } - sawOriginal = sawOriginal || store.Val.GetConstVal().GetStringValue() == "original" - sawModel = sawModel || store.Val.GetConstVal().GetStringValue() == "model" - } + for _, body := range result.program.FunctionBodies { + if body.FunctionId == toUpperID { + t.Fatal("Go server attached the model body to the base program") } - if !sawOriginal || sawModel { - t.Fatalf("package initializer was replaced by model: original=%t model=%t", sawOriginal, sawModel) - } - return - } - t.Fatal("target package initializer body missing") -} - -func TestBuildProgram_GoModelPackageRequiresOpentaintPrefix(t *testing.T) { - dir := writeTinyModule(t) - modelDir := t.TempDir() - if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module models\ngo 1.25\n"), 0o644); err != nil { - t.Fatal(err) - } - packageDir := filepath.Join(modelDir, "strings") - if err := os.MkdirAll(packageDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(packageDir, "model.go"), []byte("package strings\nfunc ToUpper(value string) string { return value }\n"), 0o644); err != nil { - t.Fatal(err) - } - - message := runBuildFatal(t, &pb.BuildProgramRequest{ - WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, - InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, - }) - if !strings.Contains(message, "import path must be opentaint/") { - t.Fatalf("unexpected validation error: %s", message) } } func TestBuildProgram_GoModelMustCompile(t *testing.T) { - dir := writeTinyModule(t) - modelDir := writeStringsModel(t, "strings") - if err := os.WriteFile(filepath.Join(modelDir, "strings", "model.go"), []byte("package strings\nfunc ToUpper(value string) string { return missing }\n"), 0o644); err != nil { - t.Fatal(err) - } - + modelDir := writeStringsModel( + t, + "package strings\nfunc ToUpper(value string) string { return missing }\n", + ) message := runBuildFatal(t, &pb.BuildProgramRequest{ - WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, + WorkingDir: writeTinyModule(t), Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, }) if !strings.Contains(message, "Go model does not compile") || !strings.Contains(message, "undefined: missing") { @@ -379,548 +246,79 @@ func TestBuildProgram_GoModelMustCompile(t *testing.T) { } } -func TestBuildProgram_GoModelReplacesReceiverMethodBody(t *testing.T) { - projectDir := t.TempDir() - if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte("module example.com/m\ngo 1.22\n"), 0o644); err != nil { - t.Fatal(err) - } - projectPackageDir := filepath.Join(projectDir, "box") - if err := os.MkdirAll(projectPackageDir, 0o755); err != nil { - t.Fatal(err) - } - projectSource := "package box\ntype Box struct{}\nfunc (b *Box) Value(value string) string { return \"original\" }\n" - if err := os.WriteFile(filepath.Join(projectPackageDir, "box.go"), []byte(projectSource), 0o644); err != nil { - t.Fatal(err) - } - - modelDir := t.TempDir() - if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { - t.Fatal(err) - } - modelPackageDir := filepath.Join(modelDir, "example.com", "m", "box") - if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { - t.Fatal(err) - } - modelSource := "package box\ntype Box struct{}\nfunc (b *Box) Value(value string) string { return value }\n" - if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { - t.Fatal(err) - } - - program := runBuildProgram(t, &pb.BuildProgramRequest{ - WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, - InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, - }) - targetPackage := findPkg(program.Packages, "example.com/m/box") - if targetPackage == nil { - t.Fatal("modeled package missing") - } - var target *pb.ProtoFunction - for _, fn := range targetPackage.Functions { - if fn.Name == "Value" { - target = fn - break - } - } - if target == nil || !target.IsMethod || !target.IsPointerReceiver { - t.Fatalf("modeled receiver method missing: %+v", target) - } - for _, body := range program.FunctionBodies { - if body.FunctionId != target.Id || len(body.Blocks) != 1 { - continue - } - instructions := body.Blocks[0].Instructions - ret := instructions[len(instructions)-1].GetReturnInst() - if ret == nil || len(ret.Results) != 1 || ret.Results[0].GetParamIndex() != 1 { - t.Fatalf("modeled method does not return its argument: %+v", ret) - } - return - } - t.Fatal("modeled receiver method body missing") -} - -func TestBuildProgram_GoModelCanBePartialAndAddFieldsAndMethods(t *testing.T) { - projectDir := t.TempDir() - if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte("module example.com/fields\ngo 1.22\n"), 0o644); err != nil { - t.Fatal(err) - } - projectSource := `package fields - -type Box struct { - Original string - Count int -} - -func (b *Box) Put(value string) { b.Original = "original" } -func (b *Box) Get() string { return b.Original } -func (b *Box) Keep(value string) string { return "original" } -` - if err := os.WriteFile(filepath.Join(projectDir, "fields.go"), []byte(projectSource), 0o644); err != nil { - t.Fatal(err) - } - - modelDir := t.TempDir() - if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { - t.Fatal(err) - } - modelPackageDir := filepath.Join(modelDir, "example.com", "fields") - if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { - t.Fatal(err) - } - modelSource := `package fields - -type Box struct { - Shadow string - Original string -} - -func (b *Box) Put(value string) { - b.Shadow = value - b.Original = value -} -func (b *Box) Get() string { return b.Shadow } -func (b *Box) Helper(value string) string { - b.Shadow = value - return b.Shadow -} -` - if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { - t.Fatal(err) - } - - program := runBuildProgram(t, &pb.BuildProgramRequest{ - WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, - InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, - }) - targetPackage := findPkg(program.Packages, "example.com/fields") - if targetPackage == nil { - t.Fatal("modeled package missing") - } - - var box *pb.ProtoNamedType - for _, named := range targetPackage.NamedTypes { - if named.Name == "Box" { - box = named - break - } - } - if box == nil { - t.Fatal("modeled Box type missing") - } - if len(box.Fields) != 3 || box.Fields[0].Name != "Original" || box.Fields[0].Index != 0 || - box.Fields[1].Name != "Count" || box.Fields[1].Index != 1 || - box.Fields[2].Name != "Shadow" || box.Fields[2].Index != 2 { - t.Fatalf("model field was not appended to Box: %+v", box.Fields) - } - var underlying *pb.ProtoStructType - for _, typeDefinition := range program.Types { - if typeDefinition.Id == box.UnderlyingTypeId { - underlying = typeDefinition.GetStructType() - break - } - } - if underlying == nil || len(underlying.Fields) != 3 || underlying.Fields[2].Name != "Shadow" { - t.Fatalf("Box underlying type does not contain the model field: %+v", underlying) - } - - functions := make(map[string]*pb.ProtoFunction) - for _, function := range targetPackage.Functions { - if function.ParentFunctionId == 0 { - functions[function.Name] = function - } - } - for _, name := range []string{"Put", "Get", "Keep", "Helper"} { - if functions[name] == nil { - t.Fatalf("function %s is missing", name) - } - } - if !functions["Helper"].IsSynthetic || functions["Helper"].SyntheticKind != "opentaint model support" { - t.Fatalf("model-only method is not synthetic: %+v", functions["Helper"]) - } - - bodies := make(map[int32]*pb.ProtoFunctionBody, len(program.FunctionBodies)) - for _, body := range program.FunctionBodies { - bodies[body.FunctionId] = body - } - for _, name := range []string{"Put", "Get", "Helper"} { - body := bodies[functions[name].Id] - if body == nil { - t.Fatalf("function %s has no model body", name) - } - var sawShadow, sawOriginal bool - for _, block := range body.Blocks { - for _, instruction := range block.Instructions { - if access := instruction.GetFieldAddr(); access != nil && access.FieldName == "Shadow" { - if access.FieldIndex != 2 { - t.Fatalf("function %s uses Shadow index %d, want 2", name, access.FieldIndex) - } - sawShadow = true - } - if access := instruction.GetFieldAddr(); access != nil && access.FieldName == "Original" { - if access.FieldIndex != 0 { - t.Fatalf("function %s uses Original index %d, want 0", name, access.FieldIndex) - } - sawOriginal = true - } - if access := instruction.GetField(); access != nil && access.FieldName == "Shadow" { - if access.FieldIndex != 2 { - t.Fatalf("function %s uses Shadow index %d, want 2", name, access.FieldIndex) - } - sawShadow = true - } - } - } - if !sawShadow { - t.Fatalf("function %s does not use the model-only field", name) - } - if name == "Put" && !sawOriginal { - t.Fatal("Put does not use the existing field") - } - } - - keepBody := bodies[functions["Keep"].Id] - if keepBody == nil { - t.Fatal("unmodeled Keep body is missing") - } - var keptOriginal bool - for _, block := range keepBody.Blocks { - for _, instruction := range block.Instructions { - result := instruction.GetReturnInst() - if result != nil && len(result.Results) == 1 && - result.Results[0].GetConstVal().GetStringValue() == "original" { - keptOriginal = true - } - } - } - if !keptOriginal { - t.Fatal("partial model changed the unmodeled Keep body") - } -} - func TestBuildProgram_GoModelCanImportTargetProjectModule(t *testing.T) { projectDir := t.TempDir() - if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte("module example.com/dependency\ngo 1.22\n"), 0o644); err != nil { - t.Fatal(err) - } - projectSource := `package dependency - -type Value struct { Text string } - -func Identity(value Value) Value { return Value{Text: "original"} } -` - if err := os.WriteFile(filepath.Join(projectDir, "dependency.go"), []byte(projectSource), 0o644); err != nil { - t.Fatal(err) - } + writeFile(t, filepath.Join(projectDir, "go.mod"), "module example.com/dependency\ngo 1.22\n") + writeFile( + t, + filepath.Join(projectDir, "dependency.go"), + "package dependency\ntype Value struct { Text string }\nfunc Identity(value Value) Value { return Value{} }\n", + ) modelDir := t.TempDir() - if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { - t.Fatal(err) - } + writeFile(t, filepath.Join(modelDir, "go.mod"), "module opentaint\ngo 1.25\n") modelPackageDir := filepath.Join(modelDir, "example.com", "dependency") if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { t.Fatal(err) } - modelSource := `package dependency - -import target "example.com/dependency" - -func Identity(value target.Value) target.Value { return value } -` - if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { - t.Fatal(err) - } + writeFile( + t, + filepath.Join(modelPackageDir, "model.go"), + "package dependency\nimport target \"example.com/dependency\"\nfunc Identity(value target.Value) target.Value { return value }\n", + ) - program := runBuildProgram(t, &pb.BuildProgramRequest{ + result := runBuildResult(t, &pb.BuildProgramRequest{ WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, }) - targetPackage := findPkg(program.Packages, "example.com/dependency") - if targetPackage == nil { - t.Fatal("target dependency package missing") + if len(result.models) != 1 { + t.Fatalf("want one raw model program, got %d", len(result.models)) } - var identity *pb.ProtoFunction - for _, function := range targetPackage.Functions { - if function.Name == "Identity" && function.ParentFunctionId == 0 { - identity = function - break - } + if findPkg(result.models[0].Program.Packages, "example.com/dependency") == nil { + t.Fatal("raw model program has no imported target project package") } - if identity == nil { - t.Fatal("target Identity function missing") - } - for _, body := range program.FunctionBodies { - if body.FunctionId != identity.Id { - continue - } - instructions := body.Blocks[0].Instructions - result := instructions[len(instructions)-1].GetReturnInst() - if result == nil || len(result.Results) != 1 || result.Results[0].GetParamIndex() != 0 { - t.Fatalf("target dependency model does not return its parameter: %+v", result) - } - return - } - t.Fatal("target dependency model body missing") } func TestBuildProgram_GoModelCanImportResolvedProjectDependency(t *testing.T) { dependencyDir := t.TempDir() - if err := os.WriteFile(filepath.Join(dependencyDir, "go.mod"), []byte("module example.com/library\ngo 1.22\n"), 0o644); err != nil { - t.Fatal(err) - } - dependencySource := `package library - -type Value struct { Text string } - -func Identity(value Value) Value { return Value{Text: "original"} } -` - if err := os.WriteFile(filepath.Join(dependencyDir, "library.go"), []byte(dependencySource), 0o644); err != nil { - t.Fatal(err) - } + writeFile(t, filepath.Join(dependencyDir, "go.mod"), "module example.com/library\ngo 1.22\n") + writeFile( + t, + filepath.Join(dependencyDir, "library.go"), + "package library\ntype Value struct { Text string }\nfunc Identity(value Value) Value { return Value{} }\n", + ) projectDir := t.TempDir() - projectGoMod := "module example.com/app\ngo 1.22\nrequire example.com/library v0.0.0\nreplace example.com/library => " + dependencyDir + "\n" - if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte(projectGoMod), 0o644); err != nil { - t.Fatal(err) - } - projectSource := `package main - -import "example.com/library" - -func Use(value library.Value) library.Value { return library.Identity(value) } -` - if err := os.WriteFile(filepath.Join(projectDir, "main.go"), []byte(projectSource), 0o644); err != nil { - t.Fatal(err) - } + projectGoMod := "module example.com/app\ngo 1.22\nrequire example.com/library v0.0.0\n" + + "replace example.com/library => " + dependencyDir + "\n" + writeFile(t, filepath.Join(projectDir, "go.mod"), projectGoMod) + writeFile( + t, + filepath.Join(projectDir, "main.go"), + "package main\nimport \"example.com/library\"\nfunc Use(value library.Value) library.Value { return library.Identity(value) }\n", + ) modelDir := t.TempDir() - if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { - t.Fatal(err) - } + writeFile(t, filepath.Join(modelDir, "go.mod"), "module opentaint\ngo 1.25\n") modelPackageDir := filepath.Join(modelDir, "example.com", "library") if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { t.Fatal(err) } - modelSource := `package library - -import target "example.com/library" - -func Identity(value target.Value) target.Value { return value } -` - if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { - t.Fatal(err) - } - - program := runBuildProgram(t, &pb.BuildProgramRequest{ - WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, - InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, - }) - targetPackage := findPkg(program.Packages, "example.com/library") - if targetPackage == nil || !targetPackage.IsDependency { - t.Fatalf("resolved project dependency is missing: %+v", targetPackage) - } - var identity *pb.ProtoFunction - for _, function := range targetPackage.Functions { - if function.Name == "Identity" && function.ParentFunctionId == 0 { - identity = function - break - } - } - if identity == nil { - t.Fatal("dependency Identity function missing") - } - for _, body := range program.FunctionBodies { - if body.FunctionId != identity.Id { - continue - } - instructions := body.Blocks[0].Instructions - result := instructions[len(instructions)-1].GetReturnInst() - if result == nil || len(result.Results) != 1 || result.Results[0].GetParamIndex() != 0 { - t.Fatalf("resolved dependency model does not return its parameter: %+v", result) - } - return - } - t.Fatal("resolved dependency model body missing") -} - -func TestBuildProgram_GoModelExistingFieldTypeMustMatch(t *testing.T) { - projectDir := t.TempDir() - if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte("module example.com/fieldtype\ngo 1.22\n"), 0o644); err != nil { - t.Fatal(err) - } - projectSource := "package fieldtype\ntype Box struct { Value string }\nfunc (b *Box) Put(value string) {}\n" - if err := os.WriteFile(filepath.Join(projectDir, "fieldtype.go"), []byte(projectSource), 0o644); err != nil { - t.Fatal(err) - } - - modelDir := t.TempDir() - if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { - t.Fatal(err) - } - modelPackageDir := filepath.Join(modelDir, "example.com", "fieldtype") - if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { - t.Fatal(err) - } - modelSource := "package fieldtype\ntype Box struct { Value int }\nfunc (b *Box) Put(value string) { b.Value = len(value) }\n" - if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { - t.Fatal(err) - } - - message := runBuildFatal(t, &pb.BuildProgramRequest{ - WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, - InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, - }) - if !strings.Contains(message, "field example.com/fieldtype.Box.Value has a different type") { - t.Fatalf("unexpected field validation error: %s", message) - } -} - -func TestBuildProgram_GoModelSupportsVersionedImportPathWithDifferentPackageName(t *testing.T) { - projectDir := t.TempDir() - if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte("module github.com/acme/widget/v2\ngo 1.22\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(projectDir, "widget.go"), []byte("package widget\nfunc Identity(value string) string { return \"original\" }\n"), 0o644); err != nil { - t.Fatal(err) - } - - modelDir := t.TempDir() - if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { - t.Fatal(err) - } - modelPackageDir := filepath.Join(modelDir, "github.com", "acme", "widget", "v2") - if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte("package widget\nfunc Identity(value string) string { return value }\n"), 0o644); err != nil { - t.Fatal(err) - } - - program := runBuildProgram(t, &pb.BuildProgramRequest{ - WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, - InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, - }) - targetPackage := findPkg(program.Packages, "github.com/acme/widget/v2") - if targetPackage == nil || targetPackage.Name != "widget" { - t.Fatalf("versioned modeled package missing: %+v", targetPackage) - } - target := targetPackage.Functions[0] - for _, body := range program.FunctionBodies { - if body.FunctionId != target.Id { - continue - } - ret := body.Blocks[0].Instructions[len(body.Blocks[0].Instructions)-1].GetReturnInst() - if ret == nil || len(ret.Results) != 1 || ret.Results[0].GetParamIndex() != 0 { - t.Fatalf("versioned package model body not applied: %+v", ret) - } - return - } - t.Fatal("versioned package model body missing") -} - -func TestBuildProgram_GoModelAddsSyntheticHelpersUsedByModelBody(t *testing.T) { - dir := writeTinyModule(t) - modelDir := t.TempDir() - if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { - t.Fatal(err) - } - modelPackageDir := filepath.Join(modelDir, "strings") - if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { - t.Fatal(err) - } - modelSource := "package strings\nfunc identity(value string) string { return value }\nfunc ToUpper(value string) string { return identity(value) }\n" - if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { - t.Fatal(err) - } - - program := runBuildProgram(t, &pb.BuildProgramRequest{ - WorkingDir: dir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, - InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, - }) - stringsPackage := findPkg(program.Packages, "strings") - var target, helper *pb.ProtoFunction - for _, fn := range stringsPackage.Functions { - switch fn.Name { - case "ToUpper": - target = fn - case "identity": - helper = fn - } - } - if target == nil || helper == nil { - t.Fatalf("target or helper missing: target=%+v helper=%+v", target, helper) - } - if !helper.IsSynthetic || helper.SyntheticKind != "opentaint model support" { - t.Fatalf("model helper is not synthetic: %+v", helper) - } + writeFile( + t, + filepath.Join(modelPackageDir, "model.go"), + "package library\nimport target \"example.com/library\"\nfunc Identity(value target.Value) target.Value { return value }\n", + ) - var targetCallsHelper, helperHasBody bool - for _, body := range program.FunctionBodies { - if body.FunctionId == helper.Id { - helperHasBody = true - } - if body.FunctionId != target.Id { - continue - } - for _, block := range body.Blocks { - for _, instruction := range block.Instructions { - call := instruction.GetCall() - if call != nil && call.GetCall().GetFunction().GetFunctionId() == helper.Id { - targetCallsHelper = true - } - } - } - } - if !targetCallsHelper || !helperHasBody { - t.Fatalf("model helper reference was not preserved: called=%t body=%t", targetCallsHelper, helperHasBody) - } -} - -func TestBuildProgram_GoModelOwnsItsAnonymousFunctions(t *testing.T) { - projectDir := t.TempDir() - if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte("module example.com/closures\ngo 1.22\n"), 0o644); err != nil { - t.Fatal(err) - } - projectSource := "package closures\nfunc Apply(value string) string { nested := func() string { return \"original\" }; return nested() }\n" - if err := os.WriteFile(filepath.Join(projectDir, "closures.go"), []byte(projectSource), 0o644); err != nil { - t.Fatal(err) - } - - modelDir := t.TempDir() - if err := os.WriteFile(filepath.Join(modelDir, "go.mod"), []byte("module opentaint\ngo 1.25\n"), 0o644); err != nil { - t.Fatal(err) - } - modelPackageDir := filepath.Join(modelDir, "example.com", "closures") - if err := os.MkdirAll(modelPackageDir, 0o755); err != nil { - t.Fatal(err) - } - modelSource := "package closures\nfunc Apply(value string) string { nested := func(input string) string { return input }; return nested(value) }\n" - if err := os.WriteFile(filepath.Join(modelPackageDir, "model.go"), []byte(modelSource), 0o644); err != nil { - t.Fatal(err) - } - - program := runBuildProgram(t, &pb.BuildProgramRequest{ + result := runBuildResult(t, &pb.BuildProgramRequest{ WorkingDir: projectDir, Patterns: []string{"./..."}, Mode: pb.LoadMode_LOAD_MODE_PROJECT, InstantiateGenerics: true, SanityCheck: true, ModelDirs: []string{modelDir}, }) - targetPackage := findPkg(program.Packages, "example.com/closures") - var target *pb.ProtoFunction - functions := functionsByID(targetPackage.Functions) - for _, fn := range targetPackage.Functions { - if fn.Name == "Apply" && fn.ParentFunctionId == 0 { - target = fn - break - } - } - if target == nil || len(target.AnonFunctionIds) != 1 { - t.Fatalf("modeled parent does not own its closure: %+v", target) + if len(result.models) != 1 { + t.Fatalf("want one raw model program, got %d", len(result.models)) } - closure := functions[target.AnonFunctionIds[0]] - if closure == nil || closure.ParentFunctionId != target.Id || len(closure.Params) != 1 || !closure.IsSynthetic { - t.Fatalf("model closure metadata not preserved: %+v", closure) - } - for _, body := range program.FunctionBodies { - if body.FunctionId == closure.Id { - return - } + dependency := findPkg(result.models[0].Program.Packages, "example.com/library") + if dependency == nil || !dependency.IsDependency { + t.Fatalf("raw model has no resolved project dependency: %+v", dependency) } - t.Fatal("model closure body missing") } diff --git a/core/opentaint-ir/go/go-ssa-server/model_merge.go b/core/opentaint-ir/go/go-ssa-server/model_merge.go deleted file mode 100644 index 26d0bfca5f..0000000000 --- a/core/opentaint-ir/go/go-ssa-server/model_merge.go +++ /dev/null @@ -1,1053 +0,0 @@ -package server - -import ( - "fmt" - "sort" - "strconv" - "strings" - - pb "github.com/opentaint/go-ir/go-ssa-server/proto/goir" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" -) - -// A model package uses opentaint/ as its import path. -// Remove opentaint/ to get the exact target import path. This rule works for -// standard and module packages such as opentaint/strings, opentaint/net/http, -// and opentaint/github.com/acme/lib. -const goModelPrefix = "opentaint/" - -type modelMergeState struct { - modeledPackages map[string]string - modeledFunctions map[int32]string -} - -func newModelMergeState() *modelMergeState { - return &modelMergeState{ - modeledPackages: make(map[string]string), - modeledFunctions: make(map[int32]string), - } -} - -type modelPathNormalizer struct { - replacements map[string]string - ordered []string - targets map[string]bool -} - -func modelNormalizer(program *pb.ProtoProgram) (*modelPathNormalizer, error) { - n := &modelPathNormalizer{ - replacements: make(map[string]string), - targets: make(map[string]bool), - } - for _, pkg := range program.Packages { - if pkg.IsDependency || pkg.IsStdlib || pkg.ImportPath == universePackagePath { - continue - } - if !strings.HasPrefix(pkg.ImportPath, goModelPrefix) { - return nil, fmt.Errorf( - "Go model package %q is not model-addressed; its import path must be %s", - pkg.ImportPath, goModelPrefix, - ) - } - target := strings.TrimPrefix(pkg.ImportPath, goModelPrefix) - if target == "" { - return nil, fmt.Errorf("Go model package %q has an empty target package", pkg.ImportPath) - } - n.replacements[pkg.ImportPath] = target - n.targets[target] = true - n.ordered = append(n.ordered, pkg.ImportPath) - } - if len(n.replacements) == 0 { - return nil, fmt.Errorf( - "Go model contains no model-addressed packages; expected at least one package under %s", - goModelPrefix, - ) - } - sort.Slice(n.ordered, func(i, j int) bool { return len(n.ordered[i]) > len(n.ordered[j]) }) - return n, nil -} - -func (n *modelPathNormalizer) path(value string) string { - if replacement, ok := n.replacements[value]; ok { - return replacement - } - return value -} - -func (n *modelPathNormalizer) symbol(value string) string { - for _, markerPath := range n.ordered { - value = strings.ReplaceAll(value, markerPath, n.replacements[markerPath]) - } - return value -} - -type protoIndex struct { - program *pb.ProtoProgram - norm *modelPathNormalizer - - packages map[int32]*pb.ProtoPackage - types map[int32]*pb.ProtoTypeDefinition - named map[int32]*pb.ProtoNamedType - funcs map[int32]*pb.ProtoFunction - globals map[int32]*pb.ProtoGlobal - consts map[int32]*pb.ProtoConst - - packageKey map[int32]string - namedKey map[int32]string - typeMemo map[int32]string - typeActive map[int32]bool -} - -func indexProto(program *pb.ProtoProgram, norm *modelPathNormalizer) *protoIndex { - idx := &protoIndex{ - program: program, - norm: norm, - packages: make(map[int32]*pb.ProtoPackage), - types: make(map[int32]*pb.ProtoTypeDefinition), - named: make(map[int32]*pb.ProtoNamedType), - funcs: make(map[int32]*pb.ProtoFunction), - globals: make(map[int32]*pb.ProtoGlobal), - consts: make(map[int32]*pb.ProtoConst), - packageKey: make(map[int32]string), - namedKey: make(map[int32]string), - typeMemo: make(map[int32]string), - typeActive: make(map[int32]bool), - } - for _, td := range program.Types { - idx.types[td.Id] = td - } - for _, pkg := range program.Packages { - idx.packages[pkg.Id] = pkg - pkgPath := pkg.ImportPath - if norm != nil { - pkgPath = norm.path(pkgPath) - } - idx.packageKey[pkg.Id] = pkgPath - for _, named := range pkg.NamedTypes { - idx.named[named.Id] = named - idx.namedKey[named.Id] = pkgPath + "." + named.Name - } - for _, fn := range pkg.Functions { - idx.funcs[fn.Id] = fn - } - for _, global := range pkg.Globals { - idx.globals[global.Id] = global - } - for _, c := range pkg.Constants { - idx.consts[c.Id] = c - } - } - return idx -} - -func (i *protoIndex) symbol(value string) string { - if i.norm == nil { - return value - } - return i.norm.symbol(value) -} - -func (i *protoIndex) typeKey(id int32) string { - if id == 0 { - return "void" - } - if key, ok := i.typeMemo[id]; ok { - return key - } - if i.typeActive[id] { - // Named references break every ordinary recursive Go type cycle. This - // fallback is for malformed or future wire types and remains stable - // across independently loaded programs. - return "" - } - td := i.types[id] - if td == nil { - return "" - } - i.typeActive[id] = true - defer delete(i.typeActive, id) - - var key string - switch { - case td.GetBasic() != nil: - key = "basic:" + strconv.Itoa(int(td.GetBasic().Kind)) - case td.GetPointer() != nil: - key = "pointer:" + i.typeKey(td.GetPointer().ElemTypeId) - case td.GetArray() != nil: - key = "array:" + strconv.FormatInt(td.GetArray().Length, 10) + ":" + i.typeKey(td.GetArray().ElemTypeId) - case td.GetSlice() != nil: - key = "slice:" + i.typeKey(td.GetSlice().ElemTypeId) - case td.GetMapType() != nil: - key = "map:" + i.typeKey(td.GetMapType().KeyTypeId) + ":" + i.typeKey(td.GetMapType().ValueTypeId) - case td.GetChanType() != nil: - key = "chan:" + strconv.Itoa(int(td.GetChanType().Direction)) + ":" + i.typeKey(td.GetChanType().ElemTypeId) - case td.GetStructType() != nil: - var b strings.Builder - b.WriteString("struct{") - for _, field := range td.GetStructType().Fields { - fmt.Fprintf(&b, "%s:%s:%t:%t:%s;", field.Name, i.typeKey(field.TypeId), field.Embedded, field.Exported, field.Tag) - } - b.WriteByte('}') - key = b.String() - case td.GetInterfaceType() != nil: - var b strings.Builder - b.WriteString("interface{") - for _, method := range td.GetInterfaceType().Methods { - fmt.Fprintf(&b, "%s:%s;", method.Name, i.typeKey(method.SignatureTypeId)) - } - for _, embedded := range td.GetInterfaceType().EmbedTypeIds { - b.WriteString("embed:") - b.WriteString(i.typeKey(embedded)) - b.WriteByte(';') - } - b.WriteByte('}') - key = b.String() - case td.GetFuncType() != nil: - fn := td.GetFuncType() - var b strings.Builder - b.WriteString("func(") - for _, param := range fn.ParamTypeIds { - b.WriteString(i.typeKey(param)) - b.WriteByte(',') - } - b.WriteString(")->(") - for _, result := range fn.ResultTypeIds { - b.WriteString(i.typeKey(result)) - b.WriteByte(',') - } - fmt.Fprintf(&b, "):variadic=%t:recv=%s", fn.Variadic, i.typeKey(fn.RecvTypeId)) - key = b.String() - case td.GetNamedRef() != nil: - ref := td.GetNamedRef() - var b strings.Builder - b.WriteString("named:") - b.WriteString(i.namedKey[ref.NamedTypeId]) - b.WriteByte('[') - for _, arg := range ref.TypeArgIds { - b.WriteString(i.typeKey(arg)) - b.WriteByte(',') - } - b.WriteByte(']') - key = b.String() - case td.GetTypeParam() != nil: - param := td.GetTypeParam() - key = fmt.Sprintf("typeparam:%s:%d:%s", param.Name, param.Index, i.typeKey(param.ConstraintTypeId)) - case td.GetTuple() != nil: - var b strings.Builder - b.WriteString("tuple(") - for _, elem := range td.GetTuple().ElementTypeIds { - b.WriteString(i.typeKey(elem)) - b.WriteByte(',') - } - b.WriteByte(')') - key = b.String() - case td.GetUnsafePointer() != nil: - key = "unsafe.Pointer" - default: - key = "" - } - i.typeMemo[id] = key - return key -} - -type idRemap struct { - types map[int32]int32 - packages map[int32]int32 - functions map[int32]int32 - named map[int32]int32 - globals map[int32]int32 - consts map[int32]int32 -} - -func newIDRemap() *idRemap { - return &idRemap{ - types: make(map[int32]int32), - packages: make(map[int32]int32), - functions: make(map[int32]int32), - named: make(map[int32]int32), - globals: make(map[int32]int32), - consts: make(map[int32]int32), - } -} - -type idCounters struct { - typeID, packageID, functionID, namedID, globalID, constID int32 -} - -func countersFor(program *pb.ProtoProgram) idCounters { - var result idCounters - for _, td := range program.Types { - result.typeID = max(result.typeID, td.Id) - } - for _, pkg := range program.Packages { - result.packageID = max(result.packageID, pkg.Id) - for _, named := range pkg.NamedTypes { - result.namedID = max(result.namedID, named.Id) - } - for _, fn := range pkg.Functions { - result.functionID = max(result.functionID, fn.Id) - } - for _, global := range pkg.Globals { - result.globalID = max(result.globalID, global.Id) - } - for _, c := range pkg.Constants { - result.constID = max(result.constID, c.Id) - } - } - return result -} - -func reversePackageKeys(i *protoIndex) map[string]int32 { - result := make(map[string]int32, len(i.packageKey)) - for id, key := range i.packageKey { - result[key] = id - } - return result -} - -func typeKeysByID(i *protoIndex) map[int32]string { - result := make(map[int32]string, len(i.types)) - for id := range i.types { - result[id] = i.typeKey(id) - } - return result -} - -func functionKeysByID(i *protoIndex) map[int32]string { - result := make(map[int32]string, len(i.funcs)) - for id, fn := range i.funcs { - result[id] = i.symbol(fn.FullName) - } - return result -} - -func globalKeysByID(i *protoIndex) map[int32]string { - result := make(map[int32]string, len(i.globals)) - for id, global := range i.globals { - result[id] = i.symbol(global.FullName) - } - return result -} - -func constKeysByID(i *protoIndex) map[int32]string { - result := make(map[int32]string, len(i.consts)) - for id, c := range i.consts { - result[id] = i.symbol(c.FullName) - } - return result -} - -func reverseNamedKeys(i *protoIndex) map[string]int32 { - result := make(map[string]int32, len(i.namedKey)) - for id, key := range i.namedKey { - result[key] = id - } - return result -} - -func reverseTypeKeys(i *protoIndex) map[string]int32 { - result := make(map[string]int32, len(i.types)) - for id := range i.types { - result[i.typeKey(id)] = id - } - return result -} - -func reverseFunctionKeys(i *protoIndex) map[string]int32 { - result := make(map[string]int32, len(i.funcs)) - for id, fn := range i.funcs { - result[i.symbol(fn.FullName)] = id - } - return result -} - -func reverseGlobalKeys(i *protoIndex) map[string]int32 { - result := make(map[string]int32, len(i.globals)) - for id, global := range i.globals { - result[i.symbol(global.FullName)] = id - } - return result -} - -func reverseConstKeys(i *protoIndex) map[string]int32 { - result := make(map[string]int32, len(i.consts)) - for id, c := range i.consts { - result[i.symbol(c.FullName)] = id - } - return result -} - -func assignIDs[K comparable](source map[int32]K, existing map[K]int32, next *int32) map[int32]int32 { - result := make(map[int32]int32, len(source)) - oldIDs := make([]int, 0, len(source)) - for oldID := range source { - oldIDs = append(oldIDs, int(oldID)) - } - sort.Ints(oldIDs) - for _, rawOldID := range oldIDs { - oldID := int32(rawOldID) - key := source[oldID] - if id, ok := existing[key]; ok { - result[oldID] = id - continue - } - *next = *next + 1 - result[oldID] = *next - existing[key] = *next - } - return result -} - -func remapValue(old int32, values map[int32]int32) (int32, error) { - if old == 0 { - return 0, nil - } - value, ok := values[old] - if !ok { - return 0, fmt.Errorf("wire id %d has no model remapping", old) - } - return value, nil -} - -func remapClass(message protoreflect.Message, field protoreflect.FieldDescriptor) mapName { - name := string(field.Name()) - if name == "id" { - switch message.Descriptor().Name() { - case "ProtoTypeDefinition": - return typeMap - case "ProtoPackage": - return packageMap - case "ProtoNamedType": - return namedMap - case "ProtoFunction": - return functionMap - case "ProtoGlobal": - return globalMap - case "ProtoConst": - return constMap - } - } - if name == "named_type_id" || name == "embedded_interface_ids" { - return namedMap - } - if name == "package_id" || name == "import_ids" { - return packageMap - } - if name == "fn_id" || name == "method_ids" || name == "pointer_method_ids" || - strings.Contains(name, "function_id") { - return functionMap - } - if name == "global_id" { - return globalMap - } - if strings.HasSuffix(name, "type_id") || strings.HasSuffix(name, "type_ids") { - return typeMap - } - return noMap -} - -type mapName uint8 - -const ( - noMap mapName = iota - typeMap - packageMap - functionMap - namedMap - globalMap - constMap -) - -func (r *idRemap) values(name mapName) map[int32]int32 { - switch name { - case typeMap: - return r.types - case packageMap: - return r.packages - case functionMap: - return r.functions - case namedMap: - return r.named - case globalMap: - return r.globals - case constMap: - return r.consts - default: - return nil - } -} - -func remapMessageIDs(message protoreflect.Message, remap *idRemap) error { - var result error - message.Range(func(field protoreflect.FieldDescriptor, value protoreflect.Value) bool { - if field.IsList() { - list := value.List() - if field.Kind() == protoreflect.MessageKind { - for idx := 0; idx < list.Len(); idx++ { - if err := remapMessageIDs(list.Get(idx).Message(), remap); err != nil { - result = err - return false - } - } - return true - } - class := remapClass(message, field) - if class == noMap { - return true - } - for idx := 0; idx < list.Len(); idx++ { - mapped, err := remapValue(int32(list.Get(idx).Int()), remap.values(class)) - if err != nil { - result = fmt.Errorf("remapping %s.%s: %w", message.Descriptor().Name(), field.Name(), err) - return false - } - list.Set(idx, protoreflect.ValueOfInt32(mapped)) - } - return true - } - if field.Kind() == protoreflect.MessageKind { - if err := remapMessageIDs(value.Message(), remap); err != nil { - result = err - return false - } - return true - } - class := remapClass(message, field) - if class == noMap { - return true - } - mapped, err := remapValue(int32(value.Int()), remap.values(class)) - if err != nil { - result = fmt.Errorf("remapping %s.%s: %w", message.Descriptor().Name(), field.Name(), err) - return false - } - message.Set(field, protoreflect.ValueOfInt32(mapped)) - return true - }) - return result -} - -type modelStructFieldPlan struct { - targetNamedID int32 - indexByModel map[int32]int32 - extraFields []*pb.ProtoFieldDecl -} - -func planModelStructFields( - baseIndex, modelIndex *protoIndex, - norm *modelPathNormalizer, -) (map[int32]*modelStructFieldPlan, error) { - plans := make(map[int32]*modelStructFieldPlan) - baseNamed := reverseNamedKeys(baseIndex) - - for _, pkg := range modelIndex.program.Packages { - if _, owned := norm.replacements[pkg.ImportPath]; !owned { - continue - } - for _, modelNamed := range pkg.NamedTypes { - targetID, exists := baseNamed[modelIndex.namedKey[modelNamed.Id]] - if !exists { - continue - } - targetNamed := baseIndex.named[targetID] - if modelNamed.Kind != pb.ProtoNamedTypeKind_NAMED_TYPE_STRUCT { - continue - } - if targetNamed.Kind != pb.ProtoNamedTypeKind_NAMED_TYPE_STRUCT { - return nil, fmt.Errorf( - "go model type %s is a struct, but its target is not a struct", - modelIndex.namedKey[modelNamed.Id], - ) - } - - targetFields := make(map[string]*pb.ProtoFieldDecl, len(targetNamed.Fields)) - var nextIndex int32 - for _, field := range targetNamed.Fields { - targetFields[field.Name] = field - nextIndex = max(nextIndex, field.Index+1) - } - - plan := &modelStructFieldPlan{ - targetNamedID: targetID, - indexByModel: make(map[int32]int32, len(modelNamed.Fields)), - } - for _, modelField := range modelNamed.Fields { - modelIndexValue := modelField.Index - if targetField := targetFields[modelField.Name]; targetField != nil { - if modelIndex.typeKey(modelField.TypeId) != baseIndex.typeKey(targetField.TypeId) { - return nil, fmt.Errorf( - "go model field %s.%s has a different type from its target", - modelIndex.namedKey[modelNamed.Id], modelField.Name, - ) - } - plan.indexByModel[modelIndexValue] = targetField.Index - modelField.Index = targetField.Index - continue - } - - plan.indexByModel[modelIndexValue] = nextIndex - modelField.Index = nextIndex - plan.extraFields = append(plan.extraFields, modelField) - nextIndex++ - } - - if underlying := modelIndex.types[modelNamed.UnderlyingTypeId].GetStructType(); underlying != nil { - for _, field := range underlying.Fields { - if targetIndex, exists := plan.indexByModel[field.Index]; exists { - field.Index = targetIndex - } - } - } - plans[modelNamed.Id] = plan - } - } - - return plans, nil -} - -func modelNamedTypeForFieldAccess( - index *protoIndex, - underlyingToNamed map[int32]int32, - typeID int32, - active map[int32]bool, -) int32 { - if typeID == 0 || active[typeID] { - return 0 - } - active[typeID] = true - defer delete(active, typeID) - - td := index.types[typeID] - if td == nil { - return 0 - } - if pointer := td.GetPointer(); pointer != nil { - return modelNamedTypeForFieldAccess(index, underlyingToNamed, pointer.ElemTypeId, active) - } - if named := td.GetNamedRef(); named != nil { - return named.NamedTypeId - } - if td.GetStructType() != nil { - return underlyingToNamed[typeID] - } - return 0 -} - -func remapModelStructFieldAccesses( - model *pb.ProtoProgram, - modelIndex *protoIndex, - plans map[int32]*modelStructFieldPlan, -) error { - underlyingToNamed := make(map[int32]int32, len(modelIndex.named)) - for namedID, named := range modelIndex.named { - underlyingToNamed[named.UnderlyingTypeId] = namedID - } - - remapAccess := func(typeID, fieldIndex int32, fieldName string) (int32, error) { - namedID := modelNamedTypeForFieldAccess( - modelIndex, underlyingToNamed, typeID, make(map[int32]bool), - ) - plan := plans[namedID] - if plan == nil { - return fieldIndex, nil - } - targetIndex, exists := plan.indexByModel[fieldIndex] - if !exists { - return 0, fmt.Errorf( - "go model field access %s[%d] has no target field mapping", - fieldName, fieldIndex, - ) - } - return targetIndex, nil - } - - for _, body := range model.FunctionBodies { - for _, block := range body.Blocks { - for _, instruction := range block.Instructions { - if access := instruction.GetFieldAddr(); access != nil { - fieldIndex, err := remapAccess(access.X.TypeId, access.FieldIndex, access.FieldName) - if err != nil { - return err - } - access.FieldIndex = fieldIndex - } - if access := instruction.GetField(); access != nil { - fieldIndex, err := remapAccess(access.X.TypeId, access.FieldIndex, access.FieldName) - if err != nil { - return err - } - access.FieldIndex = fieldIndex - } - } - } - } - return nil -} - -func mergeModelStructFields( - base *pb.ProtoProgram, - baseIndex *protoIndex, - plans map[int32]*modelStructFieldPlan, - counters *idCounters, -) error { - modelNamedIDs := make([]int, 0, len(plans)) - for modelNamedID := range plans { - modelNamedIDs = append(modelNamedIDs, int(modelNamedID)) - } - sort.Ints(modelNamedIDs) - - for _, rawModelNamedID := range modelNamedIDs { - plan := plans[int32(rawModelNamedID)] - if len(plan.extraFields) == 0 { - continue - } - targetNamed := baseIndex.named[plan.targetNamedID] - if targetNamed == nil { - return fmt.Errorf("go model target named type %d is missing", plan.targetNamedID) - } - underlying := baseIndex.types[targetNamed.UnderlyingTypeId].GetStructType() - if underlying == nil { - return fmt.Errorf("go model target type %s has no struct body", targetNamed.FullName) - } - - combinedFields := make([]*pb.ProtoStructField, 0, len(underlying.Fields)+len(plan.extraFields)) - for _, field := range underlying.Fields { - combinedFields = append(combinedFields, proto.Clone(field).(*pb.ProtoStructField)) - } - for _, field := range plan.extraFields { - targetNamed.Fields = append(targetNamed.Fields, proto.Clone(field).(*pb.ProtoFieldDecl)) - combinedFields = append(combinedFields, &pb.ProtoStructField{ - Name: field.Name, - TypeId: field.TypeId, - Index: field.Index, - Embedded: field.Embedded, - Exported: field.Exported, - Tag: field.Tag, - }) - } - - counters.typeID++ - combined := &pb.ProtoTypeDefinition{ - Id: counters.typeID, - Type: &pb.ProtoTypeDefinition_StructType{StructType: &pb.ProtoStructType{ - Fields: combinedFields, - }}, - } - base.Types = append(base.Types, combined) - baseIndex.types[combined.Id] = combined - targetNamed.UnderlyingTypeId = combined.Id - } - return nil -} - -func mergeGoModel(base *pb.ProtoProgram, rawModel *pb.ProtoProgram, state *modelMergeState, source string) error { - norm, err := modelNormalizer(rawModel) - if err != nil { - return fmt.Errorf("invalid Go model %s: %w", source, err) - } - for target := range norm.targets { - if previous, exists := state.modeledPackages[target]; exists { - return fmt.Errorf("Go package %q is modeled more than once (%s and %s)", target, previous, source) - } - state.modeledPackages[target] = source - } - - model := proto.Clone(rawModel).(*pb.ProtoProgram) - baseIndex := indexProto(base, nil) - modelIndex := indexProto(model, norm) - fieldPlans, err := planModelStructFields(baseIndex, modelIndex, norm) - if err != nil { - return fmt.Errorf("invalid Go model %s: %w", source, err) - } - if err := remapModelStructFieldAccesses(model, modelIndex, fieldPlans); err != nil { - return fmt.Errorf("invalid Go model %s: %w", source, err) - } - modelOwnedOldPackages := make(map[int32]bool) - modelInitOldFunctions := make(map[int32]bool) - for _, pkg := range model.Packages { - if _, ok := norm.replacements[pkg.ImportPath]; !ok { - continue - } - modelOwnedOldPackages[pkg.Id] = true - for _, fn := range pkg.Functions { - if fn.Name == "init" || strings.HasPrefix(fn.Name, "init#") { - modelInitOldFunctions[fn.Id] = true - } - } - } - // Closures declared inside an initializer are initializer implementation - // details too. Exclude the whole descendant tree, not just the synthetic - // package initializer and the compiler-numbered init#N roots. - for changed := true; changed; { - changed = false - for _, pkg := range model.Packages { - if !modelOwnedOldPackages[pkg.Id] { - continue - } - for _, fn := range pkg.Functions { - if !modelInitOldFunctions[fn.Id] && modelInitOldFunctions[fn.ParentFunctionId] { - modelInitOldFunctions[fn.Id] = true - changed = true - } - } - } - } - basePackages := reversePackageKeys(baseIndex) - for _, pkg := range model.Packages { - target, owned := norm.replacements[pkg.ImportPath] - if !owned { - continue - } - basePackageID, targetExists := basePackages[target] - if targetExists && pkg.Name != baseIndex.packages[basePackageID].Name { - return fmt.Errorf( - "Go model package %q has package name %q, want %q (the modeled package name)", - pkg.ImportPath, pkg.Name, baseIndex.packages[basePackageID].Name, - ) - } - } - counters := countersFor(base) - remap := newIDRemap() - - modelPackageKeys := modelIndex.packageKey - modelNamedKeys := modelIndex.namedKey - modelTypeKeys := typeKeysByID(modelIndex) - modelFunctionKeys := functionKeysByID(modelIndex) - // Anonymous functions are implementation details of the modeled body, not - // declarations to match against similarly numbered closures in the target - // body. Give them fresh identities even when their generated full names - // happen to be equal. - for id, fn := range modelIndex.funcs { - if fn.ParentFunctionId != 0 { - modelFunctionKeys[id] += fmt.Sprintf("#opentaint-model-anonymous:%d", id) - } - } - modelGlobalKeys := globalKeysByID(modelIndex) - modelConstKeys := constKeysByID(modelIndex) - - remap.packages = assignIDs(modelPackageKeys, reversePackageKeys(baseIndex), &counters.packageID) - remap.named = assignIDs(modelNamedKeys, reverseNamedKeys(baseIndex), &counters.namedID) - remap.types = assignIDs(modelTypeKeys, reverseTypeKeys(baseIndex), &counters.typeID) - remap.functions = assignIDs(modelFunctionKeys, reverseFunctionKeys(baseIndex), &counters.functionID) - remap.globals = assignIDs(modelGlobalKeys, reverseGlobalKeys(baseIndex), &counters.globalID) - remap.consts = assignIDs(modelConstKeys, reverseConstKeys(baseIndex), &counters.constID) - - // A body is attached to the target declaration, so a same-named model - // must have exactly the target signature. Go has no overloads that could - // otherwise disambiguate the mismatch. - baseFunctions := reverseFunctionKeys(baseIndex) - for modelID, key := range modelFunctionKeys { - baseID, exists := baseFunctions[key] - if !exists { - continue - } - modelFn := modelIndex.funcs[modelID] - if modelFn == nil || modelFn.ParentFunctionId != 0 || modelInitOldFunctions[modelID] || - !modelOwnedPackage(modelIndex, norm, modelFn.PackageId) { - continue - } - baseFn := baseIndex.funcs[baseID] - if modelIndex.typeKey(modelFn.SignatureTypeId) != baseIndex.typeKey(baseFn.SignatureTypeId) || - modelFn.IsMethod != baseFn.IsMethod || modelFn.IsPointerReceiver != baseFn.IsPointerReceiver { - return fmt.Errorf("Go model function %s has a different signature from its target", key) - } - } - - for _, pkg := range model.Packages { - if !modelOwnedOldPackages[pkg.Id] { - continue - } - pkg.InitFunctionId = 0 - pkg.ImportPath = norm.path(pkg.ImportPath) - pkg.IsDependency = true - pkg.IsStdlib = false - } - for _, pkg := range model.Packages { - for _, named := range pkg.NamedTypes { - named.FullName = norm.symbol(named.FullName) - } - for _, fn := range pkg.Functions { - fn.FullName = norm.symbol(fn.FullName) - } - for _, global := range pkg.Globals { - global.FullName = norm.symbol(global.FullName) - } - for _, c := range pkg.Constants { - c.FullName = norm.symbol(c.FullName) - } - } - - modelOwnedPackages := make(map[int32]bool, len(modelOwnedOldPackages)) - for id := range modelOwnedOldPackages { - modelOwnedPackages[remap.packages[id]] = true - } - modelInitFunctions := make(map[int32]bool, len(modelInitOldFunctions)) - for id := range modelInitOldFunctions { - modelInitFunctions[remap.functions[id]] = true - } - - if err := remapMessageIDs(model.ProtoReflect(), remap); err != nil { - return fmt.Errorf("merging Go model %s: %w", source, err) - } - - baseIndex = indexProto(base, nil) - if err := mergeModelStructFields(base, baseIndex, fieldPlans, &counters); err != nil { - return fmt.Errorf("merging Go model %s: %w", source, err) - } - for _, td := range model.Types { - if baseIndex.types[td.Id] == nil { - base.Types = append(base.Types, td) - baseIndex.types[td.Id] = td - } - } - - for _, modelPkg := range model.Packages { - basePkg := baseIndex.packages[modelPkg.Id] - if basePkg == nil { - if modelOwnedPackages[modelPkg.Id] { - functions := modelPkg.Functions[:0] - for _, fn := range modelPkg.Functions { - if modelInitFunctions[fn.Id] { - continue - } - fn.IsSynthetic = true - fn.SyntheticKind = "opentaint model support" - functions = append(functions, fn) - } - modelPkg.Functions = functions - } - base.Packages = append(base.Packages, modelPkg) - baseIndex.packages[modelPkg.Id] = modelPkg - basePkg = modelPkg - continue - } - - basePkg.ImportIds = appendUniqueIDs(basePkg.ImportIds, modelPkg.ImportIds...) - existingNamed := namedByID(basePkg.NamedTypes) - for _, named := range modelPkg.NamedTypes { - if current := existingNamed[named.Id]; current != nil { - current.MethodIds = appendUniqueIDs(current.MethodIds, named.MethodIds...) - current.PointerMethodIds = appendUniqueIDs(current.PointerMethodIds, named.PointerMethodIds...) - continue - } - basePkg.NamedTypes = append(basePkg.NamedTypes, named) - existingNamed[named.Id] = named - } - - existingFunctions := functionsByID(basePkg.Functions) - for _, fn := range modelPkg.Functions { - if modelInitFunctions[fn.Id] { - continue - } - if current := existingFunctions[fn.Id]; current != nil { - if modelOwnedPackages[modelPkg.Id] { - current.AnonFunctionIds = fn.AnonFunctionIds - } - continue - } - if modelOwnedPackages[modelPkg.Id] { - fn.IsSynthetic = true - fn.SyntheticKind = "opentaint model support" - } - basePkg.Functions = append(basePkg.Functions, fn) - existingFunctions[fn.Id] = fn - } - mergeGlobals(&basePkg.Globals, modelPkg.Globals) - mergeConsts(&basePkg.Constants, modelPkg.Constants) - } - - baseIndex = indexProto(base, nil) - for _, body := range model.FunctionBodies { - if modelInitFunctions[body.FunctionId] { - continue - } - fn := baseIndex.funcs[body.FunctionId] - if fn == nil || !modelOwnedPackages[fn.PackageId] { - continue - } - if previous, exists := state.modeledFunctions[body.FunctionId]; exists { - return fmt.Errorf("Go function %q is modeled more than once (%s and %s)", fn.FullName, previous, source) - } - state.modeledFunctions[body.FunctionId] = source - base.FunctionBodies = replaceFunctionBody(base.FunctionBodies, body) - fn.HasBody = true - } - - return nil -} - -func modelOwnedPackage(index *protoIndex, norm *modelPathNormalizer, packageID int32) bool { - pkg := index.packages[packageID] - if pkg == nil { - return false - } - _, ok := norm.replacements[pkg.ImportPath] - return ok -} - -func appendUniqueIDs(dst []int32, values ...int32) []int32 { - seen := make(map[int32]bool, len(dst)+len(values)) - for _, value := range dst { - seen[value] = true - } - for _, value := range values { - if value != 0 && !seen[value] { - dst = append(dst, value) - seen[value] = true - } - } - return dst -} - -func namedByID(values []*pb.ProtoNamedType) map[int32]*pb.ProtoNamedType { - result := make(map[int32]*pb.ProtoNamedType, len(values)) - for _, value := range values { - result[value.Id] = value - } - return result -} - -func functionsByID(values []*pb.ProtoFunction) map[int32]*pb.ProtoFunction { - result := make(map[int32]*pb.ProtoFunction, len(values)) - for _, value := range values { - result[value.Id] = value - } - return result -} - -func mergeGlobals(dst *[]*pb.ProtoGlobal, values []*pb.ProtoGlobal) { - seen := make(map[int32]bool, len(*dst)) - for _, value := range *dst { - seen[value.Id] = true - } - for _, value := range values { - if !seen[value.Id] { - *dst = append(*dst, value) - seen[value.Id] = true - } - } -} - -func mergeConsts(dst *[]*pb.ProtoConst, values []*pb.ProtoConst) { - seen := make(map[int32]bool, len(*dst)) - for _, value := range *dst { - seen[value.Id] = true - } - for _, value := range values { - if !seen[value.Id] { - *dst = append(*dst, value) - seen[value.Id] = true - } - } -} - -func replaceFunctionBody(values []*pb.ProtoFunctionBody, replacement *pb.ProtoFunctionBody) []*pb.ProtoFunctionBody { - for idx, body := range values { - if body.FunctionId == replacement.FunctionId { - values[idx] = replacement - return values - } - } - return append(values, replacement) -} diff --git a/core/opentaint-ir/go/go-ssa-server/server.go b/core/opentaint-ir/go/go-ssa-server/server.go index 4b690b2bdc..e584542540 100644 --- a/core/opentaint-ir/go/go-ssa-server/server.go +++ b/core/opentaint-ir/go/go-ssa-server/server.go @@ -90,8 +90,10 @@ func (s *goSSAServer) BuildProgram(req *pb.BuildProgramRequest, stream pb.GoSSAS return err } } + if err := stream.Send(&pb.BuildProgramResponse{Payload: &pb.BuildProgramResponse_Program{Program: program}}); err != nil { + return fmt.Errorf("streaming program: %w", err) + } - mergeState := newModelMergeState() for _, modelDir := range req.ModelDirs { workspacePath, cleanupWorkspace, workspaceErr := createModelWorkspace( modelDir, info, projectModulePaths, @@ -131,18 +133,13 @@ func (s *goSSAServer) BuildProgram(req *pb.BuildProgramRequest, stream pb.GoSSAS return err } } - if err := mergeGoModel(program, modelProgram, mergeState, modelDir); err != nil { - return stream.Send(&pb.BuildProgramResponse{Payload: &pb.BuildProgramResponse_Error{Error: &pb.ProtoError{ - Message: err.Error(), - Fatal: true, - }}}) + if err := stream.Send(&pb.BuildProgramResponse{Payload: &pb.BuildProgramResponse_ModelProgram{ + ModelProgram: &pb.ProtoModelProgram{Source: modelDir, Program: modelProgram}, + }}); err != nil { + return fmt.Errorf("streaming Go model %s: %w", modelDir, err) } } - if err := stream.Send(&pb.BuildProgramResponse{Payload: &pb.BuildProgramResponse_Program{Program: program}}); err != nil { - return fmt.Errorf("streaming program: %w", err) - } - return stream.Send(&pb.BuildProgramResponse{ Payload: &pb.BuildProgramResponse_Summary{Summary: &pb.ProtoBuildSummary{ PackageCount: int32(len(program.Packages)), diff --git a/core/opentaint-ir/go/proto/goir/service.proto b/core/opentaint-ir/go/proto/goir/service.proto index c90833d73a..8ecfdfe464 100644 --- a/core/opentaint-ir/go/proto/goir/service.proto +++ b/core/opentaint-ir/go/proto/goir/service.proto @@ -47,6 +47,7 @@ message BuildProgramResponse { ProtoBuildSummary summary = 4; ProtoError error = 5; ProtoProgram program = 6; + ProtoModelProgram model_program = 7; } } @@ -76,6 +77,12 @@ message ProtoProgram { repeated ProtoFunctionBody function_bodies = 3; } +// A model program is loaded by the Go server and resolved by the Kotlin client. +message ProtoModelProgram { + string source = 1; + ProtoProgram program = 2; +} + // ─── Types ────────────────────────────────────────────────────────── message ProtoTypeDefinition { From fc46c6238321dd3565023ba9d7c1ffe6a2a0a758 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Sat, 29 Aug 2026 18:00:49 +0200 Subject: [PATCH 03/12] feat(go-models): load bundled models --- .../go/config/GoDefaultModelLoader.kt | 49 +++++++++++++++++++ .../opentaint/go/sast/project/AnalysisCtx.kt | 10 +++- .../sast/project/GoProjectAnalysisOptions.kt | 1 + .../go/sast/project/GoProjectAnalyzer.kt | 3 +- .../go/sast/project/GoTestProjectAnalyzer.kt | 3 +- .../jvm/sast/runner/ProjectAnalyzerRunner.kt | 4 ++ 6 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 core/opentaint-config/go-config/src/main/kotlin/org/opentaint/go/config/GoDefaultModelLoader.kt diff --git a/core/opentaint-config/go-config/src/main/kotlin/org/opentaint/go/config/GoDefaultModelLoader.kt b/core/opentaint-config/go-config/src/main/kotlin/org/opentaint/go/config/GoDefaultModelLoader.kt new file mode 100644 index 0000000000..c34ca5b980 --- /dev/null +++ b/core/opentaint-config/go-config/src/main/kotlin/org/opentaint/go/config/GoDefaultModelLoader.kt @@ -0,0 +1,49 @@ +package org.opentaint.go.config + +import java.net.URI +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.copyToRecursively +import kotlin.io.path.createTempDirectory +import kotlin.streams.asSequence + +object GoDefaultModelLoader { + private const val MODEL_ROOT = "/model/go/dataflow" + + private val unpackedModelPaths: List by lazy { + loadModelPaths() + } + + fun modelPaths(): List = unpackedModelPaths + + private fun loadModelPaths(): List { + val resource = javaClass.getResource(MODEL_ROOT) ?: return emptyList() + return when (resource.protocol) { + "file" -> findModelModules(Path.of(resource.toURI())) + "jar" -> unpackJarModels(resource.toURI()) + else -> error("Unsupported bundled Go model resource protocol: ${resource.protocol}") + } + } + + @OptIn(ExperimentalPathApi::class) + private fun unpackJarModels(resource: URI): List { + val unpacked = createTempDirectory("opentaint-go-models") + FileSystems.newFileSystem(resource, emptyMap()).use { fs -> + fs.getPath(MODEL_ROOT).copyToRecursively( + unpacked, + followLinks = false, + overwrite = false, + ) + } + return findModelModules(unpacked) + } + + private fun findModelModules(root: Path): List = Files.list(root).use { paths -> + paths.asSequence() + .filter { Files.isDirectory(it) && Files.isRegularFile(it.resolve("go.mod")) } + .sortedBy { it.fileName.toString() } + .toList() + } +} diff --git a/core/src/main/kotlin/org/opentaint/go/sast/project/AnalysisCtx.kt b/core/src/main/kotlin/org/opentaint/go/sast/project/AnalysisCtx.kt index 544b7ade0c..9bf43dec37 100644 --- a/core/src/main/kotlin/org/opentaint/go/sast/project/AnalysisCtx.kt +++ b/core/src/main/kotlin/org/opentaint/go/sast/project/AnalysisCtx.kt @@ -1,6 +1,7 @@ package org.opentaint.go.sast.project import mu.KLogging +import org.opentaint.go.config.GoDefaultModelLoader import org.opentaint.ir.go.api.GoIRProgram import org.opentaint.ir.go.client.GoIRClient import org.opentaint.ir.go.client.GoIRLoadConfig @@ -12,15 +13,22 @@ class AnalysisCtx( private val prj: GoProject, val client: GoIRClient, private val modelPaths: List = emptyList(), + private val useDefaultModels: Boolean = true, ) : AutoCloseable by client { val cp: GoIRProgram by lazy { logger.info { "Building Go IR for project: ${prj.projectDir}" } client.buildFromDir( prj.projectDir, - GoIRLoadConfig(mode = GoIRLoadMode.PROJECT, modelDirs = modelPaths), + GoIRLoadConfig( + mode = GoIRLoadMode.PROJECT, + modelDirs = defaultModelPaths() + modelPaths, + ), ).program } + private fun defaultModelPaths(): List = + if (useDefaultModels) GoDefaultModelLoader.modelPaths() else emptyList() + companion object { private val logger = object : KLogging() {}.logger } diff --git a/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalysisOptions.kt b/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalysisOptions.kt index 52d9d1081b..caf525b871 100644 --- a/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalysisOptions.kt +++ b/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalysisOptions.kt @@ -6,4 +6,5 @@ import java.nio.file.Path data class GoProjectAnalysisOptions( val common: CommonAnalysisOptions = CommonAnalysisOptions(), val modelPaths: List = emptyList(), + val useDefaultModels: Boolean = true, ) diff --git a/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalyzer.kt b/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalyzer.kt index f469ec8202..ca771304b2 100644 --- a/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalyzer.kt +++ b/core/src/main/kotlin/org/opentaint/go/sast/project/GoProjectAnalyzer.kt @@ -31,10 +31,11 @@ class GoProjectAnalyzer( goOptions.common ) { private val modelPaths = goOptions.modelPaths + private val useDefaultModels = goOptions.useDefaultModels override fun initializeProjectAnalysisContext(): AnalysisCtx { val client = GoIRClient() - return AnalysisCtx(project, client, modelPaths) + return AnalysisCtx(project, client, modelPaths, useDefaultModels) } override fun AnalysisCtx.selectProjectEntryPoints(): List { diff --git a/core/src/main/kotlin/org/opentaint/go/sast/project/GoTestProjectAnalyzer.kt b/core/src/main/kotlin/org/opentaint/go/sast/project/GoTestProjectAnalyzer.kt index 7841f870f4..4c4eaf4b30 100644 --- a/core/src/main/kotlin/org/opentaint/go/sast/project/GoTestProjectAnalyzer.kt +++ b/core/src/main/kotlin/org/opentaint/go/sast/project/GoTestProjectAnalyzer.kt @@ -44,6 +44,7 @@ class GoTestProjectAnalyzer( project, results, providedOptions.common.copy(storeSummaries = false), ) { private val modelPaths = providedOptions.modelPaths + private val useDefaultModels = providedOptions.useDefaultModels private val loadedRules = options.loadSemgrepRules(GoLanguageStrategy()) @Serializable @@ -78,7 +79,7 @@ class GoTestProjectAnalyzer( Yaml().decodeFromStream(it) } - val ctx = AnalysisCtx(project, GoIRClient(), modelPaths) + val ctx = AnalysisCtx(project, GoIRClient(), modelPaths, useDefaultModels) val results = ctx.analyzeTestSamples(ruleTests.tests) ?: return ProjectAnalysisStatus.EXCEPTION diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/runner/ProjectAnalyzerRunner.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/runner/ProjectAnalyzerRunner.kt index 0610fad009..c6a69d44e4 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/runner/ProjectAnalyzerRunner.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/runner/ProjectAnalyzerRunner.kt @@ -65,6 +65,9 @@ class ProjectAnalyzerRunner : AbstractAnalyzerRunner() { .directory() .multiple() + private val disableDefaultGoModels: Boolean by option(help = "Disable bundled Go models") + .flag(default = false) + private val semgrepRuleLoadTrace: Path? by option(help = "Output file for Semgrep rules loader trace") .newFile() @@ -133,6 +136,7 @@ class ProjectAnalyzerRunner : AbstractAnalyzerRunner() { override fun goOptions() = GoProjectAnalysisOptions( common = commonOptions, modelPaths = goModels, + useDefaultModels = !disableDefaultGoModels, ) private fun collectYamlConfigs(path: Path): List = path.walk() From 0c0fc740d8898e772328ccc539f0c24b07e903d1 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Sat, 29 Aug 2026 18:37:51 +0200 Subject: [PATCH 04/12] fix(go-models): preserve dependency model semantics --- .../dataflow/go/rules/GoTaintConfiguration.kt | 7 +- .../go/rules/GoTaintConfigurationTest.kt | 44 ++ .../ir/go/client/GoIRDeserializer.kt | 15 + .../opentaint/ir/go/client/GoModelMerger.kt | 399 +++++++++++++++++- .../opentaint/ir/go/impl/GoIRFunctionImpl.kt | 3 +- .../opentaint/ir/go/impl/GoIRNamedTypeImpl.kt | 3 +- .../ir/go/test/features/ModelTests.kt | 267 +++++++++++- core/opentaint-ir/go/go-ssa-server/go.mod | 2 +- .../go/go-ssa-server/model_workspace.go | 27 ++ .../go/go-ssa-server/model_workspace_test.go | 40 ++ .../go/go-ssa-server/serializer.go | 42 ++ core/opentaint-ir/go/proto/goir/service.proto | 3 + .../go/sast/dataflow/GoUnitResolver.kt | 9 +- 13 files changed, 843 insertions(+), 18 deletions(-) create mode 100644 core/opentaint-dataflow-core/opentaint-go-dataflow/src/test/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfigurationTest.kt create mode 100644 core/opentaint-ir/go/go-ssa-server/model_workspace_test.go diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfiguration.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfiguration.kt index af624f2256..9dc7fb71e9 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfiguration.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfiguration.kt @@ -179,7 +179,12 @@ class GoTaintConfiguration : GoTaintRulesProvider { patternRules: List, ruleNameMatcher: R.() -> GoNameMatcher, ): List { - val direct = simpleByName[name].orEmpty() + val genericOriginName = name.substringBefore('[', name) + val direct = if (genericOriginName == name) { + simpleByName[name].orEmpty() + } else { + simpleByName[name].orEmpty() + simpleByName[genericOriginName].orEmpty() + } val patternMatches = patternRules.filter { it.ruleNameMatcher().matches(name) } if (patternMatches.isEmpty()) return direct if (direct.isEmpty()) return patternMatches diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/test/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfigurationTest.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/test/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfigurationTest.kt new file mode 100644 index 0000000000..7f96c85384 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/test/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfigurationTest.kt @@ -0,0 +1,44 @@ +package org.opentaint.dataflow.go.rules + +import org.opentaint.dataflow.configuration.go.serialized.GoNameMatcher +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedPassAction +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedRule +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedTaintConfig +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Result +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers.BaseOnly +import org.opentaint.dataflow.go.GoFunctionSignature +import org.opentaint.ir.go.type.GoIRBasicType +import org.opentaint.ir.go.type.GoIRBasicTypeKind.STRING +import org.opentaint.ir.go.type.GoIRPointerType +import kotlin.test.Test +import kotlin.test.assertEquals + +class GoTaintConfigurationTest { + @Test + fun `simple rule name matches a generic function instance`() { + val stringType = GoIRBasicType(STRING) + val rule = GoSerializedRule.PassThrough( + pkg = GoNameMatcher.Simple("github.com/google/go-github/v89/github"), + function = GoNameMatcher.Simple("Ptr"), + copy = listOf( + GoSerializedPassAction( + from = BaseOnly(Argument(0)), + to = BaseOnly(Result), + ), + ), + ) + val configuration = GoTaintConfiguration().also { + it.loadConfig(GoSerializedTaintConfig(passThrough = listOf(rule))) + } + val signature = GoFunctionSignature( + name = "github.com/google/go-github/v89/github.Ptr[string]", + receiverType = null, + paramTypes = listOf(stringType), + resultType = GoIRPointerType(stringType), + pkgName = "github.com/google/go-github/v89/github", + ) + + assertEquals(1, configuration.passThroughForFunction(signature).size) + } +} diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt index bcd541a8d7..c21bad7545 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt @@ -10,6 +10,7 @@ import org.opentaint.ir.go.api.GoIRPackage import org.opentaint.ir.go.api.GoIRParameter import org.opentaint.ir.go.api.GoIRPosition import org.opentaint.ir.go.api.GoIRProgram +import org.opentaint.ir.go.api.GoIRTypeParamDecl import org.opentaint.ir.go.api.GoIrFunctionReference import org.opentaint.ir.go.cfg.GoIRCallInfo import org.opentaint.ir.go.cfg.GoIRCallTarget @@ -356,6 +357,13 @@ class GoIRDeserializer { underlying = resolveType(nt.underlyingTypeId), kind = namedTypeKindFromProto(nt.kind), position = positionFromProto(nt.position), + typeParams = nt.typeParamsList.map { typeParam -> + GoIRTypeParamDecl( + typeParam.name, + typeParam.index, + resolveType(typeParam.constraintTypeId), + ) + }, ) for (fd in nt.fieldsList) { @@ -556,6 +564,13 @@ class GoIRDeserializer { declaredHasBody = pf.hasBody, parent = parentFunction, anonymousFunctions = anonymousFunctions, + typeParams = pf.typeParamsList.map { typeParam -> + GoIRTypeParamDecl( + typeParam.name, + typeParam.index, + getType(typeParam.constraintTypeId), + ) + }, ) } } diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt index 8e5f94865d..3fc74c95bd 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt @@ -17,7 +17,6 @@ import org.opentaint.ir.go.proto.ProtoStructType import org.opentaint.ir.go.proto.ProtoTypeDefinition internal class GoModelMerger { - private val modeledPackages = mutableMapOf() private val modeledFunctions = mutableMapOf() fun merge(base: ProtoProgram, models: List): ProtoProgram { @@ -33,13 +32,6 @@ internal class GoModelMerger { throw IllegalArgumentException("invalid Go model $source: ${error.message}", error) } - normalizer.targets.forEach { target -> - val previous = modeledPackages.putIfAbsent(target, source) - require(previous == null) { - "Go package \"$target\" is modeled more than once ($previous and $source)" - } - } - var model = rawModel var baseIndex = ProtoIndex(base) var modelIndex = ProtoIndex(model, normalizer) @@ -56,6 +48,12 @@ internal class GoModelMerger { val ownedOldPackages = model.packagesList .filter { it.importPath in normalizer.replacements } .mapTo(mutableSetOf()) { it.id } + val importedTargetPackages = model.packagesList + .filter { it.id !in ownedOldPackages && it.importPath in normalizer.targets } + .mapTo(mutableSetOf()) { it.id } + val importedTargetFunctions = modelIndex.functions.values + .filter { it.packageId in importedTargetPackages } + .mapTo(mutableSetOf()) { it.id } val initOldFunctions = collectInitializerFunctions(model, ownedOldPackages) val basePackages = baseIndex.reversePackageKeys() @@ -111,6 +109,32 @@ internal class GoModelMerger { ) model = normalizeModel(model, normalizer, ownedOldPackages) + model = model.toBuilder() + .clearFunctionBodies() + .addAllFunctionBodies( + model.functionBodiesList.filterNot { it.functionId in importedTargetFunctions }, + ) + .clearPackages() + .addAllPackages(model.packagesList.map { pkg -> + if (pkg.id !in importedTargetPackages) { + pkg + } else { + pkg.toBuilder() + .setInitFunctionId(0) + .clearFunctions() + .clearGlobals() + .clearConstants() + .clearNamedTypes() + .addAllNamedTypes(pkg.namedTypesList.map { named -> + named.toBuilder() + .clearMethodIds() + .clearPointerMethodIds() + .build() + }) + .build() + } + }) + .build() val ownedPackages = ownedOldPackages.mapTo(mutableSetOf()) { remap.packages.getValue(it) } val initFunctions = initOldFunctions.mapTo(mutableSetOf()) { remap.functions.getValue(it) } model = try { @@ -130,6 +154,8 @@ internal class GoModelMerger { result = mergePackages(result, model, ownedPackages, initFunctions) baseIndex = ProtoIndex(result) result = mergeFunctionBodies(result, model, baseIndex, ownedPackages, initFunctions, source) + result = mergeGenericInstanceBodies(result, ownedPackages, source, counters) + validateReferences(result, source) return result } @@ -434,7 +460,17 @@ internal class GoModelMerger { .clearFunctions() .addAllFunctions(pkg.functionsList.map { function -> if (function.id in functionsWithBodies) { - function.toBuilder().setHasBody(true).build() + function.toBuilder() + .setHasBody(true) + .setIsSynthetic(true) + .setSyntheticKind( + if (function.syntheticKind == MODEL_SUPPORT_KIND) { + MODEL_SUPPORT_KIND + } else { + MODEL_KIND + }, + ) + .build() } else { function } @@ -450,12 +486,278 @@ internal class GoModelMerger { .build() } + private fun mergeGenericInstanceBodies( + program: ProtoProgram, + ownedPackages: Set, + source: String, + counters: IdCounters, + ): ProtoProgram { + val index = ProtoIndex(program) + val genericOriginIds = index.functions.values + .mapNotNullTo(mutableSetOf()) { it.originFunctionId.takeIf { id -> id != 0 } } + val modeledGenericBodies = program.functionBodiesList.mapNotNull { body -> + val function = index.functions[body.functionId] ?: return@mapNotNull null + if ( + function.packageId !in ownedPackages || + function.id !in genericOriginIds || + modeledFunctions[function.id] != source || + function.syntheticKind != MODEL_KIND + ) { + return@mapNotNull null + } + function.id to ModeledGenericBody(function, body) + }.toMap() + if (modeledGenericBodies.isEmpty()) { + return program + } + + val bodies = program.functionBodiesList.toMutableList() + val bodyIndices = bodies.mapIndexed { position, body -> body.functionId to position } + .toMap() + .toMutableMap() + val bodiesByFunction = program.functionBodiesList.associateBy { it.functionId } + val modeledInstances = mutableSetOf() + val instanceAnonymousFunctions = mutableMapOf>() + val addedFunctions = mutableListOf() + val types = program.typesList.toMutableList() + index.functions.values.forEach { function -> + val modeled = modeledGenericBodies[function.originFunctionId] ?: return@forEach + val previous = modeledFunctions.putIfAbsent(function.id, source) + require(previous == null) { + "Go function \"${function.fullName}\" is modeled more than once ($previous and $source)" + } + val typeArgumentsByIndex = modeled.function.typeParamsList.associate { typeParam -> + require(typeParam.index in function.typeArgIdsList.indices) { + "Go generic instance ${function.fullName} has no type argument for " + + "parameter ${typeParam.name}[${typeParam.index}]" + } + typeParam.index to function.typeArgIdsList[typeParam.index] + } + val specializer = GenericTypeSpecializer(index.types, typeArgumentsByIndex, counters) + val anonymousIds = collectAnonymousFunctionIds(modeled.function, index.functions) + val functionIds = anonymousIds.associateWith { counters.nextFunction() }.toMutableMap() + functionIds[modeled.function.id] = function.id + val functionRemap = { id: Int -> functionIds[id] ?: id } + val body = specializer.specialize(modeled.body, functionRemap) + anonymousIds.forEach { anonymousId -> + val original = index.functions.getValue(anonymousId) + val suffix = function.name.removePrefix(modeled.function.name) + .ifEmpty { "#${function.id}" } + val specializedFunction = specializer.specialize(original, functionRemap) + .toBuilder() + .setName(original.name + suffix) + .setFullName(original.fullName + suffix) + .setIsSynthetic(true) + .setSyntheticKind(MODEL_SUPPORT_KIND) + .build() + addedFunctions += specializedFunction + + val originalBody = bodiesByFunction[anonymousId] + require(originalBody != null) { + "Go model anonymous function ${original.fullName} has no body" + } + val specializedBody = specializer.specialize(originalBody, functionRemap) + val anonymousBodyPosition = bodyIndices[specializedBody.functionId] + if (anonymousBodyPosition == null) { + bodyIndices[specializedBody.functionId] = bodies.size + bodies += specializedBody + } else { + bodies[anonymousBodyPosition] = specializedBody + } + } + instanceAnonymousFunctions[function.id] = modeled.function.anonFunctionIdsList.map(functionRemap) + types += specializer.addedTypes + val position = bodyIndices[function.id] + if (position == null) { + bodyIndices[function.id] = bodies.size + bodies += body + } else { + bodies[position] = body + } + modeledInstances += function.id + } + if (modeledInstances.isEmpty()) { + return program + } + + val packages = program.packagesList.map { pkg -> + val packageAdditions = addedFunctions.filter { it.packageId == pkg.id } + if (pkg.functionsList.none { it.id in modeledInstances } && packageAdditions.isEmpty()) { + pkg + } else { + pkg.toBuilder() + .clearFunctions() + .addAllFunctions(pkg.functionsList.map { function -> + if (function.id !in modeledInstances) { + function + } else { + function.toBuilder() + .setHasBody(true) + .setIsSynthetic(true) + .setSyntheticKind(MODEL_KIND) + .clearAnonFunctionIds() + .addAllAnonFunctionIds(instanceAnonymousFunctions.getValue(function.id)) + .build() + } + }) + .addAllFunctions(packageAdditions) + .build() + } + } + return program.toBuilder() + .clearTypes() + .addAllTypes(types) + .clearPackages() + .addAllPackages(packages) + .clearFunctionBodies() + .addAllFunctionBodies(bodies) + .build() + } + private fun asModelSupport(function: ProtoFunction): ProtoFunction { return function.toBuilder() .setIsSynthetic(true) .setSyntheticKind(MODEL_SUPPORT_KIND) .build() } + + private data class ModeledGenericBody( + val function: ProtoFunction, + val body: ProtoFunctionBody, + ) + + private fun collectAnonymousFunctionIds( + root: ProtoFunction, + functions: Map, + ): List { + val result = linkedSetOf() + val pending = ArrayDeque(root.anonFunctionIdsList) + while (pending.isNotEmpty()) { + val id = pending.removeFirst() + if (!result.add(id)) { + continue + } + val function = functions[id] + ?: throw IllegalArgumentException("Go model anonymous function $id is missing") + pending.addAll(function.anonFunctionIdsList) + } + return result.toList() + } +} + +private class GenericTypeSpecializer( + private val definitions: Map, + private val typeArgumentsByIndex: Map, + private val counters: IdCounters, +) { + private val specialized = mutableMapOf() + private val active = mutableSetOf() + val addedTypes = mutableListOf() + + fun specialize(body: ProtoFunctionBody, remapFunction: (Int) -> Int): ProtoFunctionBody = + remapMessageIdsOfClass( + remapMessageIdsOfClass(body, MapName.TYPE, ::specializeType), + MapName.FUNCTION, + remapFunction, + ) as ProtoFunctionBody + + fun specialize(function: ProtoFunction, remapFunction: (Int) -> Int): ProtoFunction = + remapMessageIdsOfClass( + remapMessageIdsOfClass(function, MapName.TYPE, ::specializeType), + MapName.FUNCTION, + remapFunction, + ) as ProtoFunction + + private fun specializeType(id: Int): Int { + if (id == 0) { + return 0 + } + specialized[id]?.let { return it } + val definition = definitions[id] + ?: throw IllegalArgumentException("Go model type $id is missing during generic specialization") + if (definition.typeCase == ProtoTypeDefinition.TypeCase.TYPE_PARAM) { + typeArgumentsByIndex[definition.typeParam.index]?.let { typeArgument -> + specialized[id] = typeArgument + return typeArgument + } + } + require(active.add(id)) { "recursive Go model type $id cannot be specialized" } + val rewritten = rewrite(definition) + active -= id + if (rewritten == definition) { + specialized[id] = id + return id + } + val result = rewritten.toBuilder().setId(counters.nextType()).build() + addedTypes += result + specialized[id] = result.id + return result.id + } + + private fun rewrite(definition: ProtoTypeDefinition): ProtoTypeDefinition { + val builder = definition.toBuilder() + when (definition.typeCase) { + ProtoTypeDefinition.TypeCase.POINTER -> builder.setPointer( + definition.pointer.toBuilder().setElemTypeId(specializeType(definition.pointer.elemTypeId)), + ) + ProtoTypeDefinition.TypeCase.ARRAY -> builder.setArray( + definition.array.toBuilder().setElemTypeId(specializeType(definition.array.elemTypeId)), + ) + ProtoTypeDefinition.TypeCase.SLICE -> builder.setSlice( + definition.slice.toBuilder().setElemTypeId(specializeType(definition.slice.elemTypeId)), + ) + ProtoTypeDefinition.TypeCase.MAP_TYPE -> builder.setMapType( + definition.mapType.toBuilder() + .setKeyTypeId(specializeType(definition.mapType.keyTypeId)) + .setValueTypeId(specializeType(definition.mapType.valueTypeId)), + ) + ProtoTypeDefinition.TypeCase.CHAN_TYPE -> builder.setChanType( + definition.chanType.toBuilder().setElemTypeId(specializeType(definition.chanType.elemTypeId)), + ) + ProtoTypeDefinition.TypeCase.STRUCT_TYPE -> builder.setStructType( + definition.structType.toBuilder() + .clearFields() + .addAllFields(definition.structType.fieldsList.map { field -> + field.toBuilder().setTypeId(specializeType(field.typeId)).build() + }), + ) + ProtoTypeDefinition.TypeCase.INTERFACE_TYPE -> builder.setInterfaceType( + definition.interfaceType.toBuilder() + .clearMethods() + .addAllMethods(definition.interfaceType.methodsList.map { method -> + method.toBuilder() + .setSignatureTypeId(specializeType(method.signatureTypeId)) + .build() + }) + .clearEmbedTypeIds() + .addAllEmbedTypeIds(definition.interfaceType.embedTypeIdsList.map(::specializeType)), + ) + ProtoTypeDefinition.TypeCase.FUNC_TYPE -> builder.setFuncType( + definition.funcType.toBuilder() + .clearParamTypeIds() + .addAllParamTypeIds(definition.funcType.paramTypeIdsList.map(::specializeType)) + .clearResultTypeIds() + .addAllResultTypeIds(definition.funcType.resultTypeIdsList.map(::specializeType)) + .setRecvTypeId(specializeType(definition.funcType.recvTypeId)), + ) + ProtoTypeDefinition.TypeCase.NAMED_REF -> builder.setNamedRef( + definition.namedRef.toBuilder() + .clearTypeArgIds() + .addAllTypeArgIds(definition.namedRef.typeArgIdsList.map(::specializeType)), + ) + ProtoTypeDefinition.TypeCase.TYPE_PARAM -> {} + ProtoTypeDefinition.TypeCase.TUPLE -> builder.setTuple( + definition.tuple.toBuilder() + .clearElementTypeIds() + .addAllElementTypeIds(definition.tuple.elementTypeIdsList.map(::specializeType)), + ) + ProtoTypeDefinition.TypeCase.BASIC, + ProtoTypeDefinition.TypeCase.UNSAFE_POINTER, + ProtoTypeDefinition.TypeCase.TYPE_NOT_SET, + -> {} + } + return builder.build() + } } private data class ModelPathNormalizer( @@ -916,6 +1218,37 @@ private fun remapMessageIds(message: Message, remap: IdRemap): Message { return builder.build() } +private fun remapMessageIdsOfClass( + message: Message, + mapName: MapName, + remap: (Int) -> Int, +): Message { + val builder = message.toBuilder() + message.allFields.forEach { (field, value) -> + if (field.isRepeated) { + val values = value as List<*> + if (field.javaType == FieldDescriptor.JavaType.MESSAGE) { + builder.clearField(field) + values.forEach { child -> + builder.addRepeatedField( + field, + remapMessageIdsOfClass(child as Message, mapName, remap), + ) + } + } else if (remapClass(message, field) == mapName) { + values.forEachIndexed { index, oldId -> + builder.setRepeatedField(field, index, remap(oldId as Int)) + } + } + } else if (field.javaType == FieldDescriptor.JavaType.MESSAGE) { + builder.setField(field, remapMessageIdsOfClass(value as Message, mapName, remap)) + } else if (remapClass(message, field) == mapName) { + builder.setField(field, remap(value as Int)) + } + } + return builder.build() +} + private fun remapValue( oldId: Int, values: Map, @@ -931,6 +1264,51 @@ private fun remapValue( ) } +private fun validateReferences(program: ProtoProgram, source: String) { + val validIds = mapOf( + MapName.TYPE to program.typesList.mapTo(mutableSetOf()) { it.id }, + MapName.PACKAGE to program.packagesList.mapTo(mutableSetOf()) { it.id }, + MapName.FUNCTION to program.packagesList + .flatMapTo(mutableSetOf()) { pkg -> pkg.functionsList.map { it.id } }, + MapName.NAMED to program.packagesList + .flatMapTo(mutableSetOf()) { pkg -> pkg.namedTypesList.map { it.id } }, + MapName.GLOBAL to program.packagesList + .flatMapTo(mutableSetOf()) { pkg -> pkg.globalsList.map { it.id } }, + MapName.CONST to program.packagesList + .flatMapTo(mutableSetOf()) { pkg -> pkg.constantsList.map { it.id } }, + ) + + fun validate(message: Message, path: String) { + message.allFields.forEach { (field, value) -> + if (field.javaType == FieldDescriptor.JavaType.MESSAGE) { + if (field.isRepeated) { + (value as List<*>).forEachIndexed { index, child -> + validate(child as Message, "$path.${field.name}[$index]") + } + } else { + validate(value as Message, "$path.${field.name}") + } + return@forEach + } + val mapName = remapClass(message, field) + if (mapName == MapName.NONE) { + return@forEach + } + val idsInProgram = validIds.getValue(mapName) + val ids = if (field.isRepeated) value as List<*> else listOf(value) + ids.forEach { rawId -> + val id = rawId as Int + require(id == 0 || id in idsInProgram) { + "merging Go model $source: $path.${field.name} references missing " + + "${mapName.name.lowercase()} $id" + } + } + } + } + + validate(program, "ProtoProgram") +} + private fun remapClass(message: Message, field: FieldDescriptor): MapName { val name = field.name if (name == "id") { @@ -950,7 +1328,7 @@ private fun remapClass(message: Message, field: FieldDescriptor): MapName { name == "fn_id" || name == "method_ids" || name == "pointer_method_ids" || "function_id" in name -> MapName.FUNCTION name == "global_id" -> MapName.GLOBAL - name.endsWith("type_id") || name.endsWith("type_ids") -> MapName.TYPE + name == "type_arg_ids" || name.endsWith("type_id") || name.endsWith("type_ids") -> MapName.TYPE else -> MapName.NONE } } @@ -984,3 +1362,4 @@ private fun Map.reverse(): MutableMap { private const val GO_MODEL_PREFIX = "opentaint/" private const val UNIVERSE_PACKAGE_PATH = "" private const val MODEL_SUPPORT_KIND = "opentaint model support" +private const val MODEL_KIND = "opentaint model" diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt index 305e9f26ce..22cb1bf862 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt @@ -27,6 +27,7 @@ class GoIRFunctionImpl( private val declaredHasBody: Boolean = false, override val parent: GoIrFunctionReference?, override val anonymousFunctions: List, + override val typeParams: List = emptyList(), ) : GoIRFunction { private var _body: GoIRBody? = null @@ -41,8 +42,6 @@ class GoIRFunctionImpl( override val hasBody: Boolean get() = declaredHasBody override val bodyAvailable: Boolean get() = _body != null - override val typeParams: List = emptyList() // TODO: implement - fun setBody(body: GoIRBody) { this._body = body } diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRNamedTypeImpl.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRNamedTypeImpl.kt index 32d69d1edf..97fc11861b 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRNamedTypeImpl.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRNamedTypeImpl.kt @@ -17,6 +17,7 @@ class GoIRNamedTypeImpl( override val underlying: GoIRType, override val kind: GoIRNamedTypeKind, override val position: GoIRPosition?, + override val typeParams: List = emptyList(), ) : GoIRNamedType { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -37,7 +38,5 @@ class GoIRNamedTypeImpl( override val pointerMethods: List get() = _pointerMethods override val interfaceMethods: List get() = _interfaceMethods override val embeddedInterfaces: List get() = _embeddedInterfaces - override val typeParams: List = emptyList() - override fun toString(): String = "GoIRNamedType($fullName)" } diff --git a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt index 99fff2d3eb..0900ad5e0c 100644 --- a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt +++ b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt @@ -8,6 +8,7 @@ import org.opentaint.ir.go.cfg.GoIRCallTarget import org.opentaint.ir.go.client.GoIRLoadConfig import org.opentaint.ir.go.client.GoIRLoadMode import org.opentaint.ir.go.expr.GoIRFieldAddrExpr +import org.opentaint.ir.go.expr.GoIRMakeClosureExpr import org.opentaint.ir.go.ext.findExpressions import org.opentaint.ir.go.ext.findInstructions import org.opentaint.ir.go.inst.GoIRCall @@ -133,7 +134,238 @@ class ModelTests { } @Test - fun `one package cannot be modeled twice`(builder: GoIRTestBuilder) { + fun `one package can have disjoint partial models`(builder: GoIRTestBuilder) { + val project = project( + "example.com/app", + "package main\nimport \"strings\"\n" + + "func Upper(value string) string { return strings.ToUpper(value) }\n" + + "func Lower(value string) string { return strings.ToLower(value) }\n", + ) + val firstModel = model( + "strings", + "package strings\nfunc ToUpper(value string) string { return value }\n", + ) + val secondModel = model( + "strings", + "package strings\nfunc ToLower(value string) string { return value }\n", + ) + + val program = builder.buildFromDir( + project, + GoIRLoadConfig( + mode = GoIRLoadMode.PROJECT, + modelDirs = listOf(firstModel, secondModel), + ), + ) + val strings = program.findPackage("strings")!! + + listOf("ToUpper", "ToLower").forEach { name -> + val function = strings.functions.single { it.name == name } + val returned = (function.body!!.blocks.single().terminator as GoIRReturn).results.single() + assertThat(returned).isInstanceOf(GoIRParameterValue::class.java) + } + } + + @Test + fun `one package can have disjoint generic partial models`(builder: GoIRTestBuilder) { + val project = project( + "example.com/app", + """ + package main + import "slices" + func Delete(values []string) []string { + return slices.DeleteFunc(values, func(string) bool { return false }) + } + func Contains(values []string) bool { + return slices.ContainsFunc(values, func(string) bool { return true }) + } + """.trimIndent(), + ) + val firstModel = model( + "slices", + "package slices\nfunc DeleteFunc[S ~[]E, E any](values S, _ func(E) bool) S { return values }\n", + ) + val secondModel = model( + "slices", + "package slices\nfunc ContainsFunc[S ~[]E, E any](_ S, predicate func(E) bool) bool { " + + "var value E; return predicate(value) }\n", + ) + + val program = builder.buildFromDir( + project, + GoIRLoadConfig( + mode = GoIRLoadMode.PROJECT, + modelDirs = listOf(firstModel, secondModel), + ), + ) + val modeledInstances = program.findPackage("slices")!!.functions.filter { + it.fullName.startsWith("slices.DeleteFunc[") || + it.fullName.startsWith("slices.ContainsFunc[") + } + + assertThat(modeledInstances).hasSize(2) + assertThat(modeledInstances).allMatch { it.syntheticKind == "opentaint model" } + } + + @Test + fun `generic model instance body uses concrete target types`(builder: GoIRTestBuilder) { + val project = project( + "example.com/app", + "package main\nimport \"slices\"\nfunc Clone(values []string) []string { return slices.Clone(values) }\n", + ) + val model = model( + "slices", + "package slices\nfunc Clone[S ~[]E, E any](values S) S { return values }\n", + ) + + val program = builder.buildFromDir(project, modelConfig(model)) + val clone = program.findPackage("slices")!!.functions.single { + it.fullName.startsWith("slices.Clone[") + } + val returned = (clone.body!!.blocks.single().terminator as GoIRReturn) + .results.single() as GoIRParameterValue + + assertThat(clone.typeParams.map { it.name }).containsExactly("S", "E") + assertThat(returned.type).isEqualTo(clone.params.single().type) + assertThat(returned.type).isEqualTo(clone.signature.results.single()) + } + + @Test + fun `generic model specializes compound body types`(builder: GoIRTestBuilder) { + val project = project( + "example.com/genericmodel", + """ + package genericmodel + type Box[T any] struct { Value T } + func Flow[T any](value T) ( + *T, []T, map[string]T, chan T, + [1]T, struct { Value T }, func(T) T, Box[T], + ) { + var array [1]T + var record struct { Value T } + var callback func(T) T + var box Box[T] + return nil, nil, nil, nil, array, record, callback, box + } + func Use(value string) { Flow[string](value) } + """.trimIndent(), + ) + val model = model( + "example.com/genericmodel", + """ + package genericmodel + type Box[T any] struct { Value T } + func Flow[T any](value T) ( + *T, []T, map[string]T, chan T, + [1]T, struct { Value T }, func(T) T, Box[T], + ) { + pointer := &value + values := []T{value} + index := map[string]T{"value": value} + stream := make(chan T, 1) + stream <- value + array := [1]T{value} + record := struct { Value T }{Value: value} + callback := func(input T) T { return input } + box := Box[T]{Value: value} + return pointer, values, index, stream, array, record, callback, box + } + """.trimIndent(), + ) + + val program = builder.buildFromDir(project, modelConfig(model)) + val flow = program.findPackage("example.com/genericmodel")!!.functions.single { + it.fullName.startsWith("example.com/genericmodel.Flow[") + } + val returned = (flow.body!!.blocks.single { block -> + block.terminator is GoIRReturn + }.terminator as GoIRReturn).results + + assertThat(returned.map { it.type }).containsExactlyElementsOf(flow.signature.results) + } + + @Test + fun `generic receiver model instance uses concrete target types`(builder: GoIRTestBuilder) { + val project = project( + "example.com/genericreceiver", + """ + package genericreceiver + type Box[T any] struct { value T } + func (box Box[T]) Get() T { return box.value } + func Use(box Box[string]) string { return box.Get() } + """.trimIndent(), + ) + val model = model( + "example.com/genericreceiver", + """ + package genericreceiver + type Box[T any] struct { value T } + func (box Box[T]) Get() T { return box.value } + """.trimIndent(), + ) + + val program = builder.buildFromDir(project, modelConfig(model)) + val functions = program.findPackage("example.com/genericreceiver")!!.functions + val get = functions.single { + it.fullName.contains("Box[string]") && it.name.startsWith("Get[") + } + val returned = (get.body!!.blocks.single().terminator as GoIRReturn).results.single() + + assertThat(returned.type).isEqualTo(get.signature.results.single()) + } + + @Test + fun `generic model closure uses concrete target types`(builder: GoIRTestBuilder) { + val project = project( + "example.com/genericclosure", + """ + package genericclosure + func Apply[T any](value T) T { return value } + func UseString(value string) string { return Apply[string](value) } + func UseInt(value int) int { return Apply[int](value) } + """.trimIndent(), + ) + val model = model( + "example.com/genericclosure", + """ + package genericclosure + func Apply[T any](value T) T { + nested := func(input T) T { + inner := func() T { return value } + return inner() + } + return nested(value) + } + """.trimIndent(), + ) + + val program = builder.buildFromDir(project, modelConfig(model)) + val applyInstances = program.findPackage("example.com/genericclosure")!!.functions.filter { + it.fullName.startsWith("example.com/genericclosure.Apply[") + } + assertThat(applyInstances).hasSize(2) + applyInstances.forEach { apply -> + val outer = apply.findExpressions().single().fn + val inner = outer.findExpressions().single().fn + val concreteType = apply.signature.results.single() + + assertThat(outer.parent!!.function).isSameAs(apply) + assertThat(inner.parent!!.function).isSameAs(outer) + assertThat(outer.signature.params.single()).isEqualTo(apply.signature.params.single()) + assertThat(outer.signature.results.single()).isEqualTo(concreteType) + assertThat(inner.signature.results.single()).isEqualTo(concreteType) + val returned = (inner.body!!.blocks.single().terminator as GoIRReturn).results.single() + assertThat(returned.type).isEqualTo(concreteType) + } + assertThat( + applyInstances.map { apply -> + apply.findExpressions().single().fn.fullName + }, + ).doesNotHaveDuplicates() + } + + @Test + fun `one function cannot be modeled twice`(builder: GoIRTestBuilder) { val project = project( "example.com/app", "package main\nimport \"strings\"\nfunc Use(value string) string { return strings.ToUpper(value) }\n", @@ -251,6 +483,39 @@ class ModelTests { assertThat((returned as GoIRParameterValue).paramIndex).isZero() } + @Test + fun `imported target declarations do not damage generic target types`(builder: GoIRTestBuilder) { + val project = project( + "example.com/genericdependency", + """ + package genericdependency + import _ "net/http" + type Box[T any] struct { Value T } + type Callback func(*Box[string]) + func Register(Callback) {} + """.trimIndent(), + ) + val model = model( + "example.com/genericdependency", + """ + package genericdependency + import _ "net/http" + import target "example.com/genericdependency" + func Register(callback target.Callback) { callback(nil) } + """.trimIndent(), + ) + + val program = builder.buildFromDir(project, modelConfig(model)) + val targetPackage = program.findPackage("example.com/genericdependency")!! + val register = targetPackage + .functions.single { it.name == "Register" } + + assertThat(targetPackage.findNamedType("Box")!!.typeParams.map { it.name }) + .containsExactly("T") + assertThat(register.findInstructions()) + .anyMatch { it.call.target is GoIRCallTarget.Dynamic } + } + @Test fun `model adds support declarations and keeps helper calls`(builder: GoIRTestBuilder) { val project = project( diff --git a/core/opentaint-ir/go/go-ssa-server/go.mod b/core/opentaint-ir/go/go-ssa-server/go.mod index 99d6862135..4b3492fd69 100644 --- a/core/opentaint-ir/go/go-ssa-server/go.mod +++ b/core/opentaint-ir/go/go-ssa-server/go.mod @@ -3,13 +3,13 @@ module github.com/opentaint/go-ir/go-ssa-server go 1.26.0 require ( + golang.org/x/mod v0.37.0 golang.org/x/tools v0.45.0 google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 ) require ( - golang.org/x/mod v0.37.0 // indirect golang.org/x/net v0.56.0 // indirect golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect diff --git a/core/opentaint-ir/go/go-ssa-server/model_workspace.go b/core/opentaint-ir/go/go-ssa-server/model_workspace.go index f04160d247..789adefd75 100644 --- a/core/opentaint-ir/go/go-ssa-server/model_workspace.go +++ b/core/opentaint-ir/go/go-ssa-server/model_workspace.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" + "golang.org/x/mod/modfile" "golang.org/x/tools/go/packages" ) @@ -53,6 +54,12 @@ func createModelWorkspace( if !version.IsValid(goVersion) { goVersion = "go1.25.0" } + goVersion = newerGoVersion(goVersion, moduleGoVersion(modelDir)) + for _, pkg := range projectInfo { + if pkg.Module != nil && moduleDirs[pkg.Module.Dir] { + goVersion = newerGoVersion(goVersion, pkg.Module.GoVersion) + } + } var workspace strings.Builder fmt.Fprintf(&workspace, "go %s\n\nuse (\n", strings.TrimPrefix(goVersion, "go")) @@ -69,6 +76,26 @@ func createModelWorkspace( return workspacePath, cleanup, nil } +func moduleGoVersion(moduleDir string) string { + data, err := os.ReadFile(filepath.Join(moduleDir, "go.mod")) + if err != nil { + return "" + } + parsed, err := modfile.Parse("go.mod", data, nil) + if err != nil || parsed.Go == nil { + return "" + } + return parsed.Go.Version +} + +func newerGoVersion(current, candidate string) string { + candidate = "go" + strings.TrimPrefix(candidate, "go") + if version.IsValid(candidate) && version.Compare(candidate, current) > 0 { + return candidate + } + return current +} + func modelModulePaths(modelDir string, modelInfo map[string]*packages.Package) map[string]bool { result := make(map[string]bool) modelDir, err := filepath.Abs(modelDir) diff --git a/core/opentaint-ir/go/go-ssa-server/model_workspace_test.go b/core/opentaint-ir/go/go-ssa-server/model_workspace_test.go new file mode 100644 index 0000000000..500190cd33 --- /dev/null +++ b/core/opentaint-ir/go/go-ssa-server/model_workspace_test.go @@ -0,0 +1,40 @@ +package server + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "golang.org/x/tools/go/packages" +) + +func TestCreateModelWorkspaceUsesNewestModuleGoVersion(t *testing.T) { + modelDir := t.TempDir() + projectDir := t.TempDir() + writeFile(t, filepath.Join(modelDir, "go.mod"), "module opentaint\ngo 1.98.1\n") + writeFile(t, filepath.Join(projectDir, "go.mod"), "module example.com/project\ngo 1.99.2\n") + projectInfo := map[string]*packages.Package{ + "example.com/project": { + Module: &packages.Module{ + Path: "example.com/project", + GoVersion: "1.99.2", + Main: true, + Dir: projectDir, + }, + }, + } + + workspace, cleanup, err := createModelWorkspace(modelDir, projectInfo, nil) + if err != nil { + t.Fatal(err) + } + defer cleanup() + data, err := os.ReadFile(workspace) + if err != nil { + t.Fatal(err) + } + if !strings.HasPrefix(string(data), "go 1.99.2\n") { + t.Fatalf("workspace does not use the newest Go version:\n%s", data) + } +} diff --git a/core/opentaint-ir/go/go-ssa-server/serializer.go b/core/opentaint-ir/go/go-ssa-server/serializer.go index 71e2ccbc01..43403176c5 100644 --- a/core/opentaint-ir/go/go-ssa-server/serializer.go +++ b/core/opentaint-ir/go/go-ssa-server/serializer.go @@ -354,6 +354,14 @@ func (s *serializer) collectFunction(fn *ssa.Function) { func (s *serializer) collectFunctionSignature(fn *ssa.Function) { s.ids.functionID(fn) s.collectType(fn.Signature) + if typeParams := fn.TypeParams(); typeParams != nil { + for i := 0; i < typeParams.Len(); i++ { + s.collectType(typeParams.At(i)) + } + } + for _, typeArg := range fn.TypeArgs() { + s.collectType(typeArg) + } // Collect types from parameters for _, p := range fn.Params { @@ -519,6 +527,12 @@ func (s *serializer) collectType(t types.Type) { } case *types.Named: s.collectType(ut.Underlying()) + typeParams := ut.TypeParams() + if typeParams != nil { + for i := 0; i < typeParams.Len(); i++ { + s.collectType(typeParams.At(i)) + } + } // Also collect type args for instantiated generics targs := ut.TypeArgs() if targs != nil { @@ -882,6 +896,23 @@ func (s *serializer) serializeFunction(fn *ssa.Function) *pb.ProtoFunction { pf.PackageId = s.ids.packageID(fnPkg) } pf.SignatureTypeId = s.typeID(fn.Signature) + if origin := fn.Origin(); origin != nil && origin != fn { + pf.OriginFunctionId = s.ids.functionID(origin) + } + if typeParams := fn.TypeParams(); typeParams != nil { + for i := 0; i < typeParams.Len(); i++ { + typeParam := typeParams.At(i) + pf.TypeParams = append(pf.TypeParams, &pb.ProtoTypeParamDecl{ + Name: typeParam.Obj().Name(), + Index: int32(typeParam.Index()), + ConstraintTypeId: s.typeID(typeParam.Constraint()), + TypeId: s.typeID(typeParam), + }) + } + } + for _, typeArg := range fn.TypeArgs() { + pf.TypeArgIds = append(pf.TypeArgIds, s.typeID(typeArg)) + } // Method info if recv := fn.Signature.Recv(); recv != nil { @@ -1001,6 +1032,17 @@ func (s *serializer) serializeNamedType(named *types.TypeName) *pb.ProtoNamedTyp if namedType == nil { return nt } + if typeParams := namedType.TypeParams(); typeParams != nil { + for i := 0; i < typeParams.Len(); i++ { + typeParam := typeParams.At(i) + nt.TypeParams = append(nt.TypeParams, &pb.ProtoTypeParamDecl{ + Name: typeParam.Obj().Name(), + Index: int32(typeParam.Index()), + ConstraintTypeId: s.typeID(typeParam.Constraint()), + TypeId: s.typeID(typeParam), + }) + } + } mset := s.prog.MethodSets.MethodSet(namedType) for i := 0; i < mset.Len(); i++ { fn := s.prog.MethodValue(mset.At(i)) diff --git a/core/opentaint-ir/go/proto/goir/service.proto b/core/opentaint-ir/go/proto/goir/service.proto index 8ecfdfe464..05c5647021 100644 --- a/core/opentaint-ir/go/proto/goir/service.proto +++ b/core/opentaint-ir/go/proto/goir/service.proto @@ -270,6 +270,7 @@ message ProtoTypeParamDecl { string name = 1; int32 index = 2; int32 constraint_type_id = 3; + int32 type_id = 4; } message ProtoFunction { @@ -291,6 +292,8 @@ message ProtoFunction { repeated ProtoTypeParamDecl type_params = 16; bool has_body = 17; ProtoPosition position = 18; + int32 origin_function_id = 19; + repeated int32 type_arg_ids = 20; } message ProtoParam { diff --git a/core/src/main/kotlin/org/opentaint/go/sast/dataflow/GoUnitResolver.kt b/core/src/main/kotlin/org/opentaint/go/sast/dataflow/GoUnitResolver.kt index d79c3c3083..a68e5861d5 100644 --- a/core/src/main/kotlin/org/opentaint/go/sast/dataflow/GoUnitResolver.kt +++ b/core/src/main/kotlin/org/opentaint/go/sast/dataflow/GoUnitResolver.kt @@ -10,6 +10,13 @@ class GoUnitResolver : UnitResolver { override fun resolve(method: GoIRFunction): UnitType { val pkg = method.pkg ?: return UnknownUnit - return if (pkg.isProject) GoPackageUnit(pkg.importPath) else UnknownUnit + return if ( + pkg.isProject || method.syntheticKind == "opentaint model" || + method.syntheticKind == "opentaint model support" + ) { + GoPackageUnit(pkg.importPath) + } else { + UnknownUnit + } } } From eea627b1b7300785770845153503d21233dfcf16 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Sun, 30 Aug 2026 23:24:51 +0200 Subject: [PATCH 05/12] fix(go-models): enforce one model per package --- .../opentaint/ir/go/client/GoModelMerger.kt | 35 ++++++++---- .../ir/go/test/features/ModelTests.kt | 54 ++++++++----------- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt index 3fc74c95bd..3f9d1ff7af 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt @@ -17,20 +17,30 @@ import org.opentaint.ir.go.proto.ProtoStructType import org.opentaint.ir.go.proto.ProtoTypeDefinition internal class GoModelMerger { - private val modeledFunctions = mutableMapOf() - fun merge(base: ProtoProgram, models: List): ProtoProgram { + val state = ModelMergeState() return models.fold(base) { program, model -> - mergeModel(program, model.program, model.source) + mergeModel(program, model.program, state, model.source) } } - private fun mergeModel(base: ProtoProgram, rawModel: ProtoProgram, source: String): ProtoProgram { + private fun mergeModel( + base: ProtoProgram, + rawModel: ProtoProgram, + state: ModelMergeState, + source: String, + ): ProtoProgram { val normalizer = try { ModelPathNormalizer.create(rawModel) } catch (error: IllegalArgumentException) { throw IllegalArgumentException("invalid Go model $source: ${error.message}", error) } + normalizer.replacements.values.forEach { target -> + val previous = state.modeledPackages.putIfAbsent(target, source) + require(previous == null) { + "Go package \"$target\" is modeled more than once ($previous and $source)" + } + } var model = rawModel var baseIndex = ProtoIndex(base) @@ -153,8 +163,8 @@ internal class GoModelMerger { result = mergePackages(result, model, ownedPackages, initFunctions) baseIndex = ProtoIndex(result) - result = mergeFunctionBodies(result, model, baseIndex, ownedPackages, initFunctions, source) - result = mergeGenericInstanceBodies(result, ownedPackages, source, counters) + result = mergeFunctionBodies(result, model, baseIndex, ownedPackages, initFunctions, state, source) + result = mergeGenericInstanceBodies(result, ownedPackages, state, source, counters) validateReferences(result, source) return result } @@ -424,6 +434,7 @@ internal class GoModelMerger { baseIndex: ProtoIndex, ownedPackages: Set, initFunctions: Set, + state: ModelMergeState, source: String, ): ProtoProgram { val bodies = base.functionBodiesList.toMutableList() @@ -438,7 +449,7 @@ internal class GoModelMerger { if (function.packageId !in ownedPackages) { return@forEach } - val previous = modeledFunctions.putIfAbsent(body.functionId, source) + val previous = state.modeledFunctions.putIfAbsent(body.functionId, source) require(previous == null) { "Go function \"${function.fullName}\" is modeled more than once ($previous and $source)" } @@ -489,6 +500,7 @@ internal class GoModelMerger { private fun mergeGenericInstanceBodies( program: ProtoProgram, ownedPackages: Set, + state: ModelMergeState, source: String, counters: IdCounters, ): ProtoProgram { @@ -500,7 +512,7 @@ internal class GoModelMerger { if ( function.packageId !in ownedPackages || function.id !in genericOriginIds || - modeledFunctions[function.id] != source || + state.modeledFunctions[function.id] != source || function.syntheticKind != MODEL_KIND ) { return@mapNotNull null @@ -522,7 +534,7 @@ internal class GoModelMerger { val types = program.typesList.toMutableList() index.functions.values.forEach { function -> val modeled = modeledGenericBodies[function.originFunctionId] ?: return@forEach - val previous = modeledFunctions.putIfAbsent(function.id, source) + val previous = state.modeledFunctions.putIfAbsent(function.id, source) require(previous == null) { "Go function \"${function.fullName}\" is modeled more than once ($previous and $source)" } @@ -645,6 +657,11 @@ internal class GoModelMerger { } } +private class ModelMergeState { + val modeledPackages = mutableMapOf() + val modeledFunctions = mutableMapOf() +} + private class GenericTypeSpecializer( private val definitions: Map, private val typeArgumentsByIndex: Map, diff --git a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt index 0900ad5e0c..96ffc9acf7 100644 --- a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt +++ b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt @@ -134,7 +134,7 @@ class ModelTests { } @Test - fun `one package can have disjoint partial models`(builder: GoIRTestBuilder) { + fun `one package cannot have two partial model modules`(builder: GoIRTestBuilder) { val project = project( "example.com/app", "package main\nimport \"strings\"\n" + @@ -150,24 +150,20 @@ class ModelTests { "package strings\nfunc ToLower(value string) string { return value }\n", ) - val program = builder.buildFromDir( - project, - GoIRLoadConfig( - mode = GoIRLoadMode.PROJECT, - modelDirs = listOf(firstModel, secondModel), - ), - ) - val strings = program.findPackage("strings")!! - - listOf("ToUpper", "ToLower").forEach { name -> - val function = strings.functions.single { it.name == name } - val returned = (function.body!!.blocks.single().terminator as GoIRReturn).results.single() - assertThat(returned).isInstanceOf(GoIRParameterValue::class.java) - } + assertThatThrownBy { + builder.buildFromDir( + project, + GoIRLoadConfig( + mode = GoIRLoadMode.PROJECT, + modelDirs = listOf(firstModel, secondModel), + ), + ) + }.isInstanceOf(IllegalArgumentException::class.java) + .hasMessageContaining("Go package \"strings\" is modeled more than once") } @Test - fun `one package can have disjoint generic partial models`(builder: GoIRTestBuilder) { + fun `one package cannot have two generic partial model modules`(builder: GoIRTestBuilder) { val project = project( "example.com/app", """ @@ -191,20 +187,16 @@ class ModelTests { "var value E; return predicate(value) }\n", ) - val program = builder.buildFromDir( - project, - GoIRLoadConfig( - mode = GoIRLoadMode.PROJECT, - modelDirs = listOf(firstModel, secondModel), - ), - ) - val modeledInstances = program.findPackage("slices")!!.functions.filter { - it.fullName.startsWith("slices.DeleteFunc[") || - it.fullName.startsWith("slices.ContainsFunc[") - } - - assertThat(modeledInstances).hasSize(2) - assertThat(modeledInstances).allMatch { it.syntheticKind == "opentaint model" } + assertThatThrownBy { + builder.buildFromDir( + project, + GoIRLoadConfig( + mode = GoIRLoadMode.PROJECT, + modelDirs = listOf(firstModel, secondModel), + ), + ) + }.isInstanceOf(IllegalArgumentException::class.java) + .hasMessageContaining("Go package \"slices\" is modeled more than once") } @Test @@ -365,7 +357,7 @@ class ModelTests { } @Test - fun `one function cannot be modeled twice`(builder: GoIRTestBuilder) { + fun `one package cannot be modeled twice`(builder: GoIRTestBuilder) { val project = project( "example.com/app", "package main\nimport \"strings\"\nfunc Use(value string) string { return strings.ToUpper(value) }\n", From ab4606439482e748e3ebcefaa497e45b461edcdd Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Tue, 1 Sep 2026 21:40:23 +0200 Subject: [PATCH 06/12] refactor(go-models): apply lazy Go IR overlays --- .../org/opentaint/ir/go/api/GoIRFunction.kt | 2 + .../org/opentaint/ir/go/api/GoIRProgram.kt | 3 + .../ir/go/client/GoIRDeserializer.kt | 87 +- .../ir/go/client/GoIRModelOverlay.kt | 577 +++++++ .../ir/go/client/GoIRModelSpecializer.kt | 358 +++++ .../opentaint/ir/go/client/GoModelMerger.kt | 1382 ----------------- .../ir/go/client/ModelPathNormalizer.kt | 45 + .../opentaint/ir/go/impl/GoIRFunctionImpl.kt | 26 +- .../opentaint/ir/go/impl/GoIRGlobalImpl.kt | 18 +- .../opentaint/ir/go/impl/GoIRNamedTypeImpl.kt | 9 +- .../ir/go/test/features/ModelTests.kt | 85 + 11 files changed, 1186 insertions(+), 1406 deletions(-) create mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt create mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt delete mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt create mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/ModelPathNormalizer.kt diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRFunction.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRFunction.kt index 1a8e1131a4..a1081a46bf 100644 --- a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRFunction.kt +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRFunction.kt @@ -42,6 +42,8 @@ interface GoIRFunction: CommonMethod { // Generics val typeParams: List + val originFullName: String? get() = null + val typeArgs: List get() = emptyList() override val parameters: List get() = if (freeVars.isEmpty()) params diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt index fdc1f043aa..8b98251d78 100644 --- a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt @@ -6,6 +6,9 @@ import org.opentaint.ir.go.type.GoIRAnonymousInterfaceType * Top-level entity: a loaded Go program with all its packages. */ interface GoIRProgram { + /** The program before model lookup overlays. This program is [this] when no models are active. */ + val originalProgram: GoIRProgram get() = this + val packages: Map // keyed by import path val anonymousInterfaces: Map diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt index c21bad7545..a3e203c2c2 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt @@ -157,25 +157,64 @@ class GoIRDeserializer { } check(program != null) { "Unexpected IR build issue" } - val mergedProgram = GoModelMerger().merge(program, models) + val original = deserializeProgram(program) + if (models.isEmpty()) { + return original + } + + val deserializedModels = models.map { model -> + val normalizer = try { + ModelPathNormalizer.create(model.program) + } catch (error: IllegalArgumentException) { + throw IllegalArgumentException("invalid Go model ${model.source}: ${error.message}", error) + } + val ownedPaths = normalizer.replacements.keys + val modelProgram = GoIRDeserializer().deserializeProgram( + model.program, + DeserializationConfig(normalizer, ownedPaths), + ) + GoIRModelDefinition(model.source, modelProgram, normalizer.targets) + } + return GoIRModelOverlayProgram(original, deserializedModels) + } + + private data class DeserializationConfig( + val normalizer: ModelPathNormalizer? = null, + val ownedPackagePaths: Set = emptySet(), + ) { + fun path(value: String): String = normalizer?.path(value) ?: value + fun symbol(value: String): String = normalizer?.symbol(value) ?: value + fun owns(pkg: ProtoPackage): Boolean = pkg.importPath in ownedPackagePaths + } + + private fun deserializeProgram( + serializedProgram: ProtoProgram, + config: DeserializationConfig = DeserializationConfig(), + ): GoIRProgram { val resultPackages = hashMapOf() val anonymousInterfaces = Int2ObjectOpenHashMap() val result = GoIRProgramImpl(resultPackages, anonymousInterfaces) - val pkgInfo = deserializePackages(mergedProgram.packagesList) - packages.filterNotNull().forEach { - resultPackages[it.importPath] = it + val pkgInfo = deserializePackages(serializedProgram.packagesList, config) + serializedProgram.packagesList.forEach { serializedPackage -> + val deserializedPackage = getPackage(serializedPackage.id) + val importPath = deserializedPackage.importPath + if (config.owns(serializedPackage)) { + resultPackages[importPath] = deserializedPackage + } else { + resultPackages.putIfAbsent(importPath, deserializedPackage) + } } - TypeDeserializationCtx(result, mergedProgram.typesList, pkgInfo, anonymousInterfaces) + TypeDeserializationCtx(result, serializedProgram.typesList, pkgInfo, anonymousInterfaces, config) .deserializeTypes() - FunctionDeserializationCtx(pkgInfo, result) + FunctionDeserializationCtx(pkgInfo, result, config) .deserializeFunctions() - mergedProgram.packagesList.forEach { deserializePackageMembers(it) } - mergedProgram.functionBodiesList.forEach { deserializeFunctionBody(it) } + serializedProgram.packagesList.forEach { deserializePackageMembers(it, config) } + serializedProgram.functionBodiesList.forEach { deserializeFunctionBody(it) } pkgInfo.namedTypes.forEach { resolveNamedTypeBindings(it) } return result @@ -195,8 +234,12 @@ class GoIRDeserializer { val functionPkgId: Int2IntOpenHashMap, ) - private fun deserializePackages(packagesList: List): PackageInfo { - val deserialized = arrayOfNulls(packagesList.size + 1).also { + private fun deserializePackages( + packagesList: List, + config: DeserializationConfig, + ): PackageInfo { + val maxPackageId = packagesList.maxOfOrNull { it.id } ?: 0 + val deserialized = arrayOfNulls(maxPackageId + 1).also { packages = it } @@ -208,10 +251,10 @@ class GoIRDeserializer { for (pkg in packagesList) { deserialized[pkg.id] = GoIRPackageImpl( - importPath = pkg.importPath, + importPath = config.path(pkg.importPath), name = pkg.name, - isStdlib = pkg.isStdlib, - isDependency = pkg.isDependency, + isStdlib = pkg.isStdlib && !config.owns(pkg), + isDependency = pkg.isDependency || config.owns(pkg), ) pkg.namedTypesList.forEach { @@ -233,6 +276,7 @@ class GoIRDeserializer { typesList: List, val pkgInfo: PackageInfo, val anonymousInterfaces: Int2ObjectOpenHashMap, + val config: DeserializationConfig, ) { val typeDefinitions: Array = typesList.toArrayById { id } val namedTypeDefs: Array = pkgInfo.namedTypes.toArrayById { id } @@ -352,7 +396,7 @@ class GoIRDeserializer { val namedType = GoIRNamedTypeImpl( name = nt.name, - fullName = nt.fullName, + fullName = config.symbol(nt.fullName), pkg = pkg, underlying = resolveType(nt.underlyingTypeId), kind = namedTypeKindFromProto(nt.kind), @@ -525,6 +569,7 @@ class GoIRDeserializer { private class FunctionDeserializationCtx( val packageInfo: PackageInfo, val program: GoIRProgram, + val config: DeserializationConfig, ) private fun FunctionDeserializationCtx.deserializeFunctions() { @@ -550,7 +595,7 @@ class GoIRDeserializer { deserialized[pf.id] = GoIRFunctionImpl( name = pf.name, - fullName = pf.fullName, + fullName = config.symbol(pf.fullName), pkg = pkg, signature = getFunctionalType(pf.signatureTypeId), params = pf.paramsList.map { p -> GoIRParameter(p.name, getType(p.typeId), p.index) }, @@ -571,6 +616,12 @@ class GoIRDeserializer { getType(typeParam.constraintTypeId), ) }, + originFullName = pf.originFunctionId.takeIf { it != 0 } + ?.let { originId -> + functions.getOrNull(originId)?.fullName + ?.let(config::symbol) + }, + typeArgs = pf.typeArgIdsList.map(::getType), ) } } @@ -595,7 +646,7 @@ class GoIRDeserializer { functions[id] ?: error("No function found for $id") - private fun deserializePackageMembers(pp: ProtoPackage) { + private fun deserializePackageMembers(pp: ProtoPackage, config: DeserializationConfig) { val pkg = getPackage(pp.id) for (pf in pp.functionsList) { @@ -607,7 +658,7 @@ class GoIRDeserializer { for (pg in pp.globalsList) { val global = GoIRGlobalImpl( name = pg.name, - fullName = pg.fullName, + fullName = config.symbol(pg.fullName), type = getType(pg.typeId), pkg = pkg, isExported = pg.isExported, @@ -621,7 +672,7 @@ class GoIRDeserializer { for (pc in pp.constantsList) { val const = GoIRConstImpl( name = pc.name, - fullName = pc.fullName, + fullName = config.symbol(pc.fullName), type = getType(pc.typeId), value = constValueFromProto(pc.value), pkg = pkg, diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt new file mode 100644 index 0000000000..9603528ba1 --- /dev/null +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt @@ -0,0 +1,577 @@ +package org.opentaint.ir.go.client + +import org.opentaint.ir.go.api.GoIRConst +import org.opentaint.ir.go.api.GoIRField +import org.opentaint.ir.go.api.GoIRFunction +import org.opentaint.ir.go.api.GoIRGlobal +import org.opentaint.ir.go.api.GoIRInterfaceMethod +import org.opentaint.ir.go.api.GoIRNamedType +import org.opentaint.ir.go.api.GoIRPackage +import org.opentaint.ir.go.api.GoIRProgram +import org.opentaint.ir.go.api.GoIRTypeParamDecl +import org.opentaint.ir.go.impl.GoIRFunctionImpl +import org.opentaint.ir.go.impl.GoIRBasicBlockImpl +import org.opentaint.ir.go.impl.GoIRConstImpl +import org.opentaint.ir.go.impl.GoIRGlobalImpl +import org.opentaint.ir.go.impl.GoIRNamedTypeImpl +import org.opentaint.ir.go.expr.GoIRFieldAddrExpr +import org.opentaint.ir.go.expr.GoIRFieldExpr +import org.opentaint.ir.go.expr.GoIRFunctionValueExpr +import org.opentaint.ir.go.expr.GoIRGlobalValueExpr +import org.opentaint.ir.go.expr.GoIRMakeClosureExpr +import org.opentaint.ir.go.cfg.GoIRCallInfo +import org.opentaint.ir.go.cfg.GoIRCallTarget +import org.opentaint.ir.go.inst.GoIRAssignInst +import org.opentaint.ir.go.inst.GoIRCall +import org.opentaint.ir.go.inst.GoIRDefer +import org.opentaint.ir.go.inst.GoIRFieldStore +import org.opentaint.ir.go.inst.GoIRGlobalStore +import org.opentaint.ir.go.inst.GoIRGo +import org.opentaint.ir.go.type.GoIRAnonymousInterfaceType +import org.opentaint.ir.go.type.GoIRAnonymousInterfaceTypeRef +import org.opentaint.ir.go.type.GoIRArrayType +import org.opentaint.ir.go.type.GoIRBasicType +import org.opentaint.ir.go.type.GoIRChanType +import org.opentaint.ir.go.type.GoIRFuncType +import org.opentaint.ir.go.type.GoIRMapType +import org.opentaint.ir.go.type.GoIRNamedInterfaceType +import org.opentaint.ir.go.type.GoIRNamedTypeRef +import org.opentaint.ir.go.type.GoIRNamedTypeKind +import org.opentaint.ir.go.type.GoIRPointerType +import org.opentaint.ir.go.type.GoIRSliceType +import org.opentaint.ir.go.type.GoIRStructType +import org.opentaint.ir.go.type.GoIRStructField +import org.opentaint.ir.go.type.GoIRTupleType +import org.opentaint.ir.go.type.GoIRType +import org.opentaint.ir.go.type.GoIRTypeParamRef +import org.opentaint.ir.go.type.GoIRTypeParamType +import java.util.Collections +import java.util.IdentityHashMap + +internal data class GoIRModelDefinition( + val source: String, + val program: GoIRProgram, + val targetPaths: Set, +) + +/** A read-only view that applies partial Go models when a package is looked up. */ +internal class GoIRModelOverlayProgram( + override val originalProgram: GoIRProgram, + definitions: List, +) : GoIRProgram { + private val definitionsByTarget = linkedMapOf() + private val modelDependencies = linkedMapOf() + private val effectivePackages = mutableMapOf() + + init { + definitions.forEach { definition -> + definition.targetPaths.forEach { target -> + val previous = definitionsByTarget.putIfAbsent(target, definition) + require(previous == null) { + "Go package \"$target\" is modeled more than once " + + "(${previous?.source} and ${definition.source})" + } + } + } + definitionsByTarget.forEach { (target, definition) -> + validatePackage(target, definition) + } + definitions.forEach { definition -> + definition.program.packages.forEach { (path, pkg) -> + if (path !in definition.targetPaths && originalProgram.findPackage(path) == null) { + modelDependencies.putIfAbsent(path, pkg) + } + } + } + } + + override val packages: Map by lazy { + (originalProgram.packages.keys + modelDependencies.keys).associateWithTo(linkedMapOf()) { path -> + findPackage(path) ?: error("Go package $path disappeared during model lookup") + } + } + + override val anonymousInterfaces: Map + get() = originalProgram.anonymousInterfaces + + override fun findPackage(importPath: String): GoIRPackage? { + val original = originalProgram.findPackage(importPath) ?: return modelDependencies[importPath] + val definition = definitionsByTarget[importPath] ?: return original + return effectivePackages.getOrPut(importPath) { + createPackage(original, definition.program.findPackage(importPath)!!) + } + } + + override fun allFunctions(): List = packages.values.flatMap { it.functions } + + override fun allNamedTypes(): List = packages.values.flatMap { it.namedTypes } + + override fun mainPackage(): GoIRPackage? = packages.values.find { it.name == "main" } + + private fun validatePackage(target: String, definition: GoIRModelDefinition) { + val original = originalProgram.findPackage(target) + ?: throw IllegalArgumentException( + "invalid Go model ${definition.source}: target package \"$target\" was not found", + ) + val model = definition.program.findPackage(target) + ?: throw IllegalArgumentException( + "invalid Go model ${definition.source}: model package \"$target\" was not found", + ) + require(model.name == original.name) { + "Go model package \"opentaint/$target\" has package name " + + "\"${model.name}\", want \"${original.name}\" (the modeled package name)" + } + + val originalFunctions = original.functions.associateBy { it.fullName } + val initializers = initializerFunctions(model) + model.functions.forEach { function -> + if (function in initializers || function.parent != null) return@forEach + val targetFunction = originalFunctions[function.fullName] + if (targetFunction != null) { + require(sameSignature(function, targetFunction)) { + "Go model function ${function.fullName} has a different signature from its target" + } + } + } + validateFields(original, model, definition.source) + } + + private fun createPackage(original: GoIRPackage, model: GoIRPackage): GoIRPackage { + val initializers = initializerFunctions(model) + val modelFunctions = model.functions.filterNot { it in initializers } + val modelByName = modelFunctions + .filter { it.parent == null } + .associateBy { it.fullName } + val used = mutableSetOf() + val specializedAnonymousFunctions = mutableListOf() + val specializer = GoIRModelSpecializer(this) + val effectiveFunctions = original.functions.map { target -> + val replacement = modelByName[target.fullName] + if (replacement != null && replacement.bodyAvailable && sameSignature(replacement, target)) { + used += replacement + (replacement as? GoIRFunctionImpl)?.markAsModel(MODEL_KIND) + replacement + } else { + val genericModel = target.originFullName?.let(modelByName::get) + if (genericModel != null && genericModel.bodyAvailable) { + val specialized = specializer.specialize(genericModel, target) + specializedAnonymousFunctions += specialized.anonymousFunctions + specialized.root + } else { + target + } + } + }.toMutableList() + effectiveFunctions += specializedAnonymousFunctions + modelFunctions.filterTo(effectiveFunctions) { function -> + if (function in used) return@filterTo false + (function as? GoIRFunctionImpl)?.markAsModel(MODEL_SUPPORT_KIND) + true + } + + val effective = GoIRModelOverlayPackage(original, model, effectiveFunctions) + modelFunctions.forEach { (it as? GoIRFunctionImpl)?.rebindPackage(effective) } + specializedAnonymousFunctions.forEach { (it as? GoIRFunctionImpl)?.rebindPackage(effective) } + effectiveFunctions.filter { it.syntheticKind == MODEL_KIND } + .forEach { (it as? GoIRFunctionImpl)?.rebindPackage(effective) } + model.namedTypes.forEach { (it as? GoIRNamedTypeImpl)?.rebindPackage(effective) } + model.globals.forEach { (it as? GoIRGlobalImpl)?.rebindPackage(effective) } + model.constants.forEach { (it as? GoIRConstImpl)?.rebindPackage(effective) } + rebindModelPackage(model) + remapModelFieldAccesses(original, model) + rebindModelEntities( + effective, + modelFunctions + specializedAnonymousFunctions + + effectiveFunctions.filter { it.syntheticKind == MODEL_KIND }, + ) + return effective + } + + private fun rebindModelEntities( + effective: GoIRPackage, + functions: List, + ) { + fun resolve(function: GoIRFunction): GoIRFunction { + if (function.pkg?.importPath != effective.importPath) return function + return effective.functions.firstOrNull { candidate -> + candidate.fullName == function.fullName && sameSignature(candidate, function) + } ?: function + } + fun resolve(global: GoIRGlobal): GoIRGlobal { + if (global.pkg.importPath != effective.importPath) return global + return effective.globals.firstOrNull { it.fullName == global.fullName } ?: global + } + fun call(source: GoIRCallInfo): GoIRCallInfo { + val target = source.target + if (target !is GoIRCallTarget.Function) return source + val rebound = resolve(target.function) + return if (rebound === target.function) source else source.copy( + target = GoIRCallTarget.Function(rebound), + ) + } + + val visitedFunctions = Collections.newSetFromMap(IdentityHashMap()) + functions.filter(visitedFunctions::add).forEach { function -> + val body = function.body ?: return@forEach + body.blocks.forEach { block -> + val mutableBlock = block as? GoIRBasicBlockImpl ?: return@forEach + mutableBlock.setInstructions(block.instructions.map { instruction -> + when (instruction) { + is GoIRCall -> instruction.copy(call = call(instruction.call)) + is GoIRGo -> instruction.copy(call = call(instruction.call)) + is GoIRDefer -> instruction.copy(call = call(instruction.call)) + is GoIRGlobalStore -> instruction.copy(global = resolve(instruction.global)) + is GoIRAssignInst -> when (val expression = instruction.expr) { + is GoIRFunctionValueExpr -> instruction.copy( + expr = expression.copy(function = resolve(expression.function)), + ) + is GoIRMakeClosureExpr -> instruction.copy( + expr = expression.copy(fn = resolve(expression.fn)), + ) + is GoIRGlobalValueExpr -> instruction.copy( + expr = expression.copy(global = resolve(expression.global)), + ) + else -> instruction + } + else -> instruction + } + }) + } + } + } + + private fun rebindModelPackage(model: GoIRPackage) { + val visited = Collections.newSetFromMap(IdentityHashMap()) + fun rebind(type: GoIRType) { + if (!visited.add(type)) return + when (type) { + is GoIRPointerType -> rebind(type.elem) + is GoIRArrayType -> rebind(type.elem) + is GoIRSliceType -> rebind(type.elem) + is GoIRMapType -> { + rebind(type.key) + rebind(type.value) + } + is GoIRChanType -> rebind(type.elem) + is GoIRStructType -> { + type.structTypeRef?.program = this + type.fields.forEach { rebind(it.type) } + } + is GoIRNamedTypeRef -> { + type.typeRef.program = this + type.typeArgs.forEach(::rebind) + } + is GoIRNamedInterfaceType -> { + type.interfaceTypeRef.program = this + type.methods.forEach { rebind(it.signature) } + type.embeds.forEach(::rebind) + } + is GoIRAnonymousInterfaceTypeRef -> {} + is GoIRAnonymousInterfaceType -> { + type.methods.forEach { rebind(it.signature) } + type.embeds.forEach(::rebind) + } + is GoIRFuncType -> { + type.params.forEach(::rebind) + type.results.forEach(::rebind) + type.recv?.let(::rebind) + } + is GoIRTupleType -> type.elements.forEach(::rebind) + is GoIRTypeParamType -> rebind(type.constraint) + else -> {} + } + } + model.namedTypes.forEach { type -> + rebind(type.underlying) + type.fields.forEach { rebind(it.type) } + type.typeParams.forEach { rebind(it.constraint) } + } + model.functions.forEach { function -> + rebind(function.signature) + function.params.forEach { rebind(it.type) } + function.freeVars.forEach { rebind(it.type) } + function.typeParams.forEach { rebind(it.constraint) } + function.typeArgs.forEach(::rebind) + function.parent?.program = this + function.anonymousFunctions.forEach { it.program = this } + } + model.globals.forEach { rebind(it.type) } + model.constants.forEach { rebind(it.type) } + } + + private fun remapModelFieldAccesses(original: GoIRPackage, model: GoIRPackage) { + val plans = model.namedTypes.mapNotNull { modelType -> + val targetType = original.findNamedType(modelType.name) ?: return@mapNotNull null + val targetByName = targetType.fields.associateBy { it.name } + val extraByName = modelType.fields + .filter { it.name !in targetByName } + .mapIndexed { index, field -> field.name to targetType.fields.size + index } + .toMap() + val indexes = modelType.fields.associate { field -> + field.index to (targetByName[field.name]?.index ?: extraByName.getValue(field.name)) + } + modelType.fullName to indexes + }.toMap() + + fun namedType(type: GoIRType): String? = when (type) { + is GoIRPointerType -> namedType(type.elem) + is GoIRNamedTypeRef -> type.typeRef.fullRefName + is GoIRStructType -> type.structTypeRef?.fullRefName + else -> null + } + fun index(type: GoIRType, oldIndex: Int): Int = + namedType(type)?.let(plans::get)?.get(oldIndex) ?: oldIndex + + model.functions.forEach { function -> + val body = function.body ?: return@forEach + body.blocks.forEach { block -> + val mutableBlock = block as? GoIRBasicBlockImpl ?: return@forEach + mutableBlock.setInstructions(block.instructions.map { instruction -> + when (instruction) { + is GoIRAssignInst -> when (val expression = instruction.expr) { + is GoIRFieldAddrExpr -> instruction.copy( + expr = expression.copy(fieldIndex = index(expression.x.type, expression.fieldIndex)), + ) + is GoIRFieldExpr -> instruction.copy( + expr = expression.copy(fieldIndex = index(expression.x.type, expression.fieldIndex)), + ) + else -> instruction + } + is GoIRFieldStore -> instruction.copy( + fieldIndex = index(instruction.base.type, instruction.fieldIndex), + ) + else -> instruction + } + }) + } + } + } + + private fun validateFields(original: GoIRPackage, model: GoIRPackage, source: String) { + model.namedTypes.forEach { modelType -> + val targetType = original.findNamedType(modelType.name) ?: return@forEach + val targetFields = targetType.fields.associateBy { it.name } + modelType.fields.forEach { modelField -> + val targetField = targetFields[modelField.name] ?: return@forEach + require(sameType(modelField.type, targetField.type)) { + "invalid Go model $source: field ${targetType.fullName}.${modelField.name} " + + "has a different type from its target" + } + } + } + } + + private fun sameSignature(model: GoIRFunction, target: GoIRFunction): Boolean = + sameType(model.signature, target.signature) && + model.isMethod == target.isMethod && + model.isPointerReceiver == target.isPointerReceiver + + private fun sameType(first: GoIRType, second: GoIRType): Boolean = + sameType(first, second, mutableListOf()) + + private fun sameType( + first: GoIRType, + second: GoIRType, + active: MutableList>, + ): Boolean { + if (first === second) return true + if (active.any { (left, right) -> left === first && right === second }) return true + active += first to second + return when { + first is GoIRBasicType && second is GoIRBasicType -> first.kind == second.kind + first is GoIRPointerType && second is GoIRPointerType -> + sameType(first.elem, second.elem, active) + first is GoIRArrayType && second is GoIRArrayType -> + first.length == second.length && sameType(first.elem, second.elem, active) + first is GoIRSliceType && second is GoIRSliceType -> + sameType(first.elem, second.elem, active) + first is GoIRMapType && second is GoIRMapType -> + sameType(first.key, second.key, active) && sameType(first.value, second.value, active) + first is GoIRChanType && second is GoIRChanType -> + first.direction == second.direction && sameType(first.elem, second.elem, active) + first is GoIRStructType && second is GoIRStructType -> + first.structTypeRef?.fullRefName == second.structTypeRef?.fullRefName && + first.fields.size == second.fields.size && + first.fields.zip(second.fields).all { (left, right) -> + left.name == right.name && left.isEmbedded == right.isEmbedded && + left.tag == right.tag && sameType(left.type, right.type, active) + } + first is GoIRNamedTypeRef && second is GoIRNamedTypeRef -> + first.typeRef.fullRefName == second.typeRef.fullRefName && + sameTypes(first.typeArgs, second.typeArgs, active) + first is GoIRFuncType && second is GoIRFuncType -> + first.isVariadic == second.isVariadic && + nullableTypeEqual(first.recv, second.recv, active) && + sameTypes(first.params, second.params, active) && + sameTypes(first.results, second.results, active) + first is GoIRTupleType && second is GoIRTupleType -> + sameTypes(first.elements, second.elements, active) + first is GoIRTypeParamType && second is GoIRTypeParamType -> + first.paramIndex == second.paramIndex && first.name == second.name && + sameType(first.constraint, second.constraint, active) + first is GoIRTypeParamRef && second is GoIRTypeParamRef -> + first.paramIndex == second.paramIndex && first.name == second.name + first is GoIRAnonymousInterfaceTypeRef && second is GoIRAnonymousInterfaceTypeRef -> + sameAnonymousInterface(first.interfaceType, second.interfaceType, active) + first is GoIRAnonymousInterfaceType && second is GoIRAnonymousInterfaceType -> + sameAnonymousInterface(first, second, active) + first is GoIRNamedInterfaceType && second is GoIRNamedInterfaceType -> + first.interfaceTypeRef.fullRefName == second.interfaceTypeRef.fullRefName + else -> false + } + } + + private fun sameAnonymousInterface( + first: GoIRAnonymousInterfaceType, + second: GoIRAnonymousInterfaceType, + active: MutableList>, + ): Boolean = first.methods.size == second.methods.size && + first.methods.zip(second.methods).all { (left, right) -> + left.name == right.name && sameType(left.signature, right.signature, active) + } && sameTypes(first.embeds, second.embeds, active) + + private fun sameTypes( + first: List, + second: List, + active: MutableList>, + ): Boolean = first.size == second.size && + first.zip(second).all { (left, right) -> sameType(left, right, active) } + + private fun nullableTypeEqual( + first: GoIRType?, + second: GoIRType?, + active: MutableList>, + ): Boolean = when { + first == null -> second == null + second == null -> false + else -> sameType(first, second, active) + } + + private fun initializerFunctions(pkg: GoIRPackage): Set { + val result = pkg.functions + .filterTo(mutableSetOf()) { it.name == "init" || it.name.startsWith("init#") } + do { + val oldSize = result.size + pkg.functions.forEach { function -> + val parentName = function.parent?.fullName + if (parentName != null && result.any { it.fullName == parentName }) { + result += function + } + } + } while (result.size != oldSize) + return result + } +} + +private class GoIRModelOverlayPackage( + private val original: GoIRPackage, + private val model: GoIRPackage, + override val functions: List, +) : GoIRPackage { + override val importPath: String get() = original.importPath + override val name: String get() = original.name + override val isStdlib: Boolean get() = original.isStdlib + override val isDependency: Boolean get() = original.isDependency + + private val functionsByName = functions.groupBy { it.fullName } + + override val namedTypes: List by lazy { + val modelByName = model.namedTypes.associateBy { it.name } + val result = original.namedTypes.map { target -> + val modelType = modelByName[target.name] + if (modelType == null) target else GoIRModelOverlayNamedType(target, modelType, this, functionsByName) + }.toMutableList() + val originalNames = original.namedTypes.mapTo(mutableSetOf()) { it.name } + result += model.namedTypes.filter { it.name !in originalNames } + result + } + + override val globals: List by lazy { + appendByName(original.globals, model.globals) { it.name } + } + override val constants: List by lazy { + appendByName(original.constants, model.constants) { it.name } + } + override val imports: List by lazy { + appendByName(original.imports, model.imports) { it.importPath } + } + override val initFunction: GoIRFunction? get() = original.initFunction + + override fun findNamedType(name: String): GoIRNamedType? = namedTypes.find { it.name == name } + override fun findGlobal(name: String): GoIRGlobal? = globals.find { it.name == name } + override fun findConstant(name: String): GoIRConst? = constants.find { it.name == name } + override fun allMethods(): List = namedTypes.flatMap { it.allMethods() } + + private fun appendByName(first: List, second: List, name: (T) -> String): List { + val names = first.mapTo(mutableSetOf(), name) + return first + second.filter { names.add(name(it)) } + } +} + +private class GoIRModelOverlayNamedType( + private val original: GoIRNamedType, + private val model: GoIRNamedType, + override val pkg: GoIRPackage, + private val functionsByName: Map>, +) : GoIRNamedType { + override val name: String get() = original.name + override val fullName: String get() = original.fullName + override val underlying: GoIRType by lazy { + val originalStruct = original.underlying as? GoIRStructType + if (originalStruct == null) { + original.underlying + } else { + GoIRStructType( + fields.map { field -> + GoIRStructField( + field.name, + field.type, + field.isEmbedded, + field.tag, + ) + }, + originalStruct.structTypeRef, + ) + } + } + override val kind: GoIRNamedTypeKind get() = original.kind + override val position get() = original.position + override val typeParams: List get() = original.typeParams + + override val fields: List by lazy { + val targetByName = original.fields.associateBy { it.name } + val result = original.fields.map { field -> field.copy(enclosingType = this) }.toMutableList() + model.fields.filter { it.name !in targetByName }.forEach { field -> + result += field.copy(index = result.size, enclosingType = this) + } + result + } + + override val methods: List by lazy { effectiveMethods(original.methods, model.methods) } + override val pointerMethods: List by lazy { + effectiveMethods(original.pointerMethods, model.pointerMethods) + } + override val interfaceMethods: List get() = original.interfaceMethods + override val embeddedInterfaces: List get() = original.embeddedInterfaces + + private fun effectiveMethods( + targetMethods: List, + modelMethods: List, + ): List { + val targetNames = targetMethods.mapTo(mutableSetOf()) { it.fullName } + val result = targetMethods.map { target -> + functionsByName[target.fullName] + ?.firstOrNull() + ?: target + }.toMutableList() + modelMethods.filter { it.fullName !in targetNames }.forEach { modelMethod -> + result += functionsByName[modelMethod.fullName] + ?.firstOrNull() + ?: modelMethod + } + return result + } +} + +private const val MODEL_SUPPORT_KIND = "opentaint model support" +private const val MODEL_KIND = "opentaint model" diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt new file mode 100644 index 0000000000..675e73164f --- /dev/null +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt @@ -0,0 +1,358 @@ +package org.opentaint.ir.go.client + +import org.opentaint.ir.go.api.GoIRBody +import org.opentaint.ir.go.api.GoIRFunction +import org.opentaint.ir.go.api.GoIRParameter +import org.opentaint.ir.go.api.GoIRFreeVar +import org.opentaint.ir.go.api.GoIRProgram +import org.opentaint.ir.go.api.GoIRTypeParamDecl +import org.opentaint.ir.go.api.GoIrFunctionReference +import org.opentaint.ir.go.cfg.GoIRCallInfo +import org.opentaint.ir.go.cfg.GoIRCallTarget +import org.opentaint.ir.go.cfg.GoIRSelectState +import org.opentaint.ir.go.expr.* +import org.opentaint.ir.go.impl.GoIRBasicBlockImpl +import org.opentaint.ir.go.impl.GoIRBodyImpl +import org.opentaint.ir.go.impl.GoIRFunctionImpl +import org.opentaint.ir.go.inst.* +import org.opentaint.ir.go.type.* +import org.opentaint.ir.go.value.* +import java.util.IdentityHashMap + +internal data class SpecializedGoModelFunction( + val root: GoIRFunction, + val anonymousFunctions: List, +) + +/** Copies a generic model body and substitutes the target instance type arguments. */ +internal class GoIRModelSpecializer( + private val program: GoIRProgram, +) { + fun specialize(modelRoot: GoIRFunction, target: GoIRFunction): SpecializedGoModelFunction { + require(modelRoot.bodyAvailable) { "Go model function ${modelRoot.fullName} has no body" } + require(modelRoot.typeParams.size == target.typeArgs.size) { + "Go generic instance ${target.fullName} has ${target.typeArgs.size} type arguments, " + + "want ${modelRoot.typeParams.size}" + } + val typeArguments = modelRoot.typeParams.associate { parameter -> + parameter.index to target.typeArgs[parameter.index] + } + val specializeType = TypeSpecializer(typeArguments) + val sourceFunctions = collectAnonymousFunctions(modelRoot) + val suffix = target.name.removePrefix(modelRoot.name).ifEmpty { + "#opentaint-model-${target.fullName.hashCode().toUInt()}" + } + val functionMap = IdentityHashMap() + val anonymousRefs = IdentityHashMap>() + + fun reference(function: GoIRFunction): GoIrFunctionReference = + GoIrFunctionReference(function.pkg?.importPath.orEmpty(), function.name, function.signature) + .also { it.program = program } + + val rootRefs = mutableListOf() + val root = GoIRFunctionImpl( + name = target.name, + fullName = target.fullName, + pkg = target.pkg, + signature = target.signature, + params = target.params, + freeVars = target.freeVars, + position = modelRoot.position, + isMethod = target.isMethod, + isPointerReceiver = target.isPointerReceiver, + isExported = target.isExported, + isSynthetic = true, + syntheticKind = MODEL_KIND_FOR_SPECIALIZER, + declaredHasBody = true, + parent = target.parent, + anonymousFunctions = rootRefs, + typeParams = target.typeParams, + originFullName = target.originFullName, + typeArgs = target.typeArgs, + ) + functionMap[modelRoot] = root + anonymousRefs[modelRoot] = rootRefs + + sourceFunctions.forEach { source -> + val refs = mutableListOf() + val parent = source.parent?.function?.let(functionMap::get) + ?: error("Go model anonymous function ${source.fullName} has no copied parent") + val signature = specializeType(source.signature) as GoIRFuncType + val copied = GoIRFunctionImpl( + name = source.name + suffix, + fullName = source.fullName + suffix, + pkg = target.pkg, + signature = signature, + params = source.params.map { GoIRParameter(it.name, specializeType(it.type), it.index) }, + freeVars = source.freeVars.map { GoIRFreeVar(it.name, specializeType(it.type), it.index) }, + position = source.position, + isMethod = source.isMethod, + isPointerReceiver = source.isPointerReceiver, + isExported = source.isExported, + isSynthetic = true, + syntheticKind = MODEL_SUPPORT_KIND_FOR_SPECIALIZER, + declaredHasBody = true, + parent = reference(parent), + anonymousFunctions = refs, + typeParams = source.typeParams.map { + GoIRTypeParamDecl(it.name, it.index, specializeType(it.constraint)) + }, + ) + functionMap[source] = copied + anonymousRefs[source] = refs + } + + (listOf(modelRoot) + sourceFunctions).forEach { source -> + val refs = anonymousRefs.getValue(source) + refs += source.anonymousFunctions.map { child -> + reference(functionMap[child.function] ?: child.function) + } + } + + (listOf(modelRoot) + sourceFunctions).forEach { source -> + val copied = functionMap.getValue(source) + copied.setBody(copyBody(source.body!!, copied, specializeType, functionMap)) + } + return SpecializedGoModelFunction(root, sourceFunctions.map(functionMap::getValue)) + } + + private fun collectAnonymousFunctions(root: GoIRFunction): List { + val result = mutableListOf() + val pending = ArrayDeque(root.anonymousFunctions.map { it.function }) + while (pending.isNotEmpty()) { + val function = pending.removeFirst() + if (result.any { it === function }) continue + result += function + pending += function.anonymousFunctions.map { it.function } + } + return result + } + + private fun copyBody( + source: GoIRBody, + target: GoIRFunctionImpl, + type: TypeSpecializer, + functions: IdentityHashMap, + ): GoIRBody { + val blocks = source.blocks.map { GoIRBasicBlockImpl(it.index, it.label) } + val recover = source.recoverBlock?.let { blocks[it.index] } + val body = GoIRBodyImpl(target, blocks, recover) + val registers = IdentityHashMap() + source.instructions.filterIsInstance().forEach { definition -> + registers[definition.register] = GoIRRegister( + type(definition.register.type), + definition.register.index, + definition.register.name, + ) + } + val copier = BodyCopier(body, type, functions, registers) + source.blocks.forEach { oldBlock -> + val block = blocks[oldBlock.index] + block.setInstructions(oldBlock.instructions.map(copier::instruction)) + block.predIndices = oldBlock.predecessors.map { it.index } + block.succIndices = oldBlock.successors.map { it.index } + block.idomIndex = oldBlock.idom?.index ?: -1 + block.domineeIndices = oldBlock.dominatedBlocks.map { it.index } + } + blocks.forEach { block -> + block.resolvePredecessors(blocks) + block.resolveSuccessors(blocks) + block.resolveIdom(blocks) + block.resolveDominees(blocks) + } + return body + } +} + +private class TypeSpecializer( + private val arguments: Map, +) : (GoIRType) -> GoIRType { + private val memo = IdentityHashMap() + + override fun invoke(type: GoIRType): GoIRType { + memo[type]?.let { return it } + val result = when (type) { + is GoIRTypeParamType -> arguments[type.paramIndex] ?: type + is GoIRTypeParamRef -> arguments[type.paramIndex] ?: type + is GoIRPointerType -> GoIRPointerType(invoke(type.elem)) + is GoIRArrayType -> GoIRArrayType(invoke(type.elem), type.length) + is GoIRSliceType -> GoIRSliceType(invoke(type.elem)) + is GoIRMapType -> GoIRMapType(invoke(type.key), invoke(type.value)) + is GoIRChanType -> GoIRChanType(invoke(type.elem), type.direction) + is GoIRStructType -> GoIRStructType( + type.fields.map { GoIRStructField(it.name, invoke(it.type), it.isEmbedded, it.tag) }, + type.structTypeRef, + ) + is GoIRNamedTypeRef -> GoIRNamedTypeRef(type.typeRef, type.typeArgs.map(::invoke)) + is GoIRNamedInterfaceType -> GoIRNamedInterfaceType( + type.methods.map { GoIRInterfaceMethodSig(it.name, invoke(it.signature) as GoIRFuncType) }, + type.embeds.map(::invoke), + type.interfaceTypeRef, + ) + is GoIRAnonymousInterfaceType -> GoIRAnonymousInterfaceType( + type.id, + type.methods.map { GoIRInterfaceMethodSig(it.name, invoke(it.signature) as GoIRFuncType) }, + type.embeds.map(::invoke), + ) + is GoIRFuncType -> GoIRFuncType( + type.params.map(::invoke), + type.results.map(::invoke), + type.isVariadic, + type.recv?.let(::invoke), + ) + is GoIRTupleType -> GoIRTupleType(type.elements.map(::invoke)) + else -> type + } + memo[type] = result + return result + } +} + +private class BodyCopier( + private val body: GoIRBody, + private val type: TypeSpecializer, + private val functions: IdentityHashMap, + private val registers: IdentityHashMap, +) { + private fun value(value: GoIRValue): GoIRValue = when (value) { + is GoIRRegister -> registers[value] ?: error("No copied register ${value.index}") + is GoIRParameterValue -> GoIRParameterValue(type(value.type), value.name, value.paramIndex) + is GoIRConstValue -> GoIRConstValue(type(value.type), value.name, value.value) + else -> error("Unsupported Go IR value ${value::class.simpleName}") + } + + private fun location(source: GoInstLocation) = + GoInstLocation(body, source.index, source.blockIndex, source.position) + + private fun call(source: GoIRCallInfo): GoIRCallInfo = GoIRCallInfo( + mode = source.mode, + target = when (val target = source.target) { + is GoIRCallTarget.Function -> GoIRCallTarget.Function( + functions[target.function] ?: target.function, + ) + is GoIRCallTarget.Builtin -> GoIRCallTarget.Builtin(target.name, type(target.type)) + is GoIRCallTarget.Dynamic -> GoIRCallTarget.Dynamic(value(target.value)) + null -> null + }, + receiver = source.receiver?.let(::value), + methodName = source.methodName, + args = source.args.map(::value), + resultType = type(source.resultType), + ) + + private fun expression(source: GoIRExpr): GoIRExpr = when (source) { + is GoIRAllocExpr -> source.copy(type = type(source.type), allocType = type(source.allocType)) + is GoIRBinOpExpr -> source.copy(type = type(source.type), x = value(source.x), y = value(source.y)) + is GoIRUnOpExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRChangeTypeExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRConvertExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRMultiConvertExpr -> source.copy( + type = type(source.type), + x = value(source.x), + fromType = type(source.fromType), + toType = type(source.toType), + ) + is GoIRChangeInterfaceExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRSliceToArrayPointerExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRMakeInterfaceExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRTypeAssertExpr -> source.copy( + type = type(source.type), + x = value(source.x), + assertedType = type(source.assertedType), + ) + is GoIRMakeClosureExpr -> source.copy( + type = type(source.type), + fn = functions[source.fn] ?: source.fn, + bindings = source.bindings.map(::value), + ) + is GoIRMakeMapExpr -> source.copy(type = type(source.type), reserve = source.reserve?.let(::value)) + is GoIRMakeChanExpr -> source.copy(type = type(source.type), size = value(source.size)) + is GoIRMakeSliceExpr -> source.copy( + type = type(source.type), + len = value(source.len), + cap = value(source.cap), + ) + is GoIRFieldAddrExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRFieldExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRIndexAddrExpr -> source.copy( + type = type(source.type), x = value(source.x), indexValue = value(source.indexValue), + ) + is GoIRIndexExpr -> source.copy( + type = type(source.type), x = value(source.x), indexValue = value(source.indexValue), + ) + is GoIRSliceExpr -> source.copy( + type = type(source.type), + x = value(source.x), + low = source.low?.let(::value), + high = source.high?.let(::value), + max = source.max?.let(::value), + ) + is GoIRLookupExpr -> source.copy( + type = type(source.type), x = value(source.x), indexValue = value(source.indexValue), + ) + is GoIRRangeExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRNextExpr -> source.copy(type = type(source.type), iter = value(source.iter)) + is GoIRSelectExpr -> source.copy( + type = type(source.type), + states = source.states.map { + GoIRSelectState(it.direction, value(it.chan), it.send?.let(::value), it.position) + }, + ) + is GoIRExtractExpr -> source.copy(type = type(source.type), tuple = value(source.tuple)) + is GoIRGlobalValueExpr -> source.copy(type = type(source.type)) + is GoIRFunctionValueExpr -> source.copy( + type = type(source.type), function = functions[source.function] ?: source.function, + ) + is GoIRBuiltinValueExpr -> source.copy(type = type(source.type)) + is GoIRFreeVarValueExpr -> source.copy(type = type(source.type)) + } + + fun instruction(source: GoIRInst): GoIRInst { + val location = location(source.location) + return when (source) { + is GoIRAssignInst -> GoIRAssignInst(location, registers.getValue(source.register), expression(source.expr)) + is GoIRPhi -> GoIRPhi( + location, + registers.getValue(source.register), + source.edges.mapValues { value(it.value) }, + source.comment, + ) + is GoIRCall -> GoIRCall(location, registers.getValue(source.register), call(source.call)) + is GoIRJump -> source.copy(location = location) + is GoIRIf -> source.copy(location = location, cond = value(source.cond)) + is GoIRReturn -> source.copy(location = location, results = source.results.map(::value)) + is GoIRPanic -> source.copy(location = location, x = value(source.x)) + is GoIRFieldStore -> source.copy( + location = location, + addr = value(source.addr), + base = value(source.base), + value = value(source.value), + ) + is GoIRIndexStore -> source.copy( + location = location, + addr = value(source.addr), + base = value(source.base), + index = value(source.index), + value = value(source.value), + ) + is GoIRStore -> source.copy( + location = location, addr = value(source.addr), value = value(source.value), + ) + is GoIRGlobalStore -> source.copy(location = location, value = value(source.value)) + is GoIRMapUpdate -> source.copy( + location = location, + map = value(source.map), + key = value(source.key), + value = value(source.value), + ) + is GoIRSend -> source.copy(location = location, chan = value(source.chan), x = value(source.x)) + is GoIRGo -> source.copy(location = location, call = call(source.call)) + is GoIRDefer -> source.copy(location = location, call = call(source.call)) + is GoIRRunDefers -> source.copy(location = location) + is GoIRDebugRef -> source.copy(location = location, x = value(source.x)) + } + } +} + +private const val MODEL_KIND_FOR_SPECIALIZER = "opentaint model" +private const val MODEL_SUPPORT_KIND_FOR_SPECIALIZER = "opentaint model support" diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt deleted file mode 100644 index 3f9d1ff7af..0000000000 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoModelMerger.kt +++ /dev/null @@ -1,1382 +0,0 @@ -package org.opentaint.ir.go.client - -import com.google.protobuf.Descriptors.FieldDescriptor -import com.google.protobuf.Message -import org.opentaint.ir.go.proto.ProtoConst -import org.opentaint.ir.go.proto.ProtoFieldDecl -import org.opentaint.ir.go.proto.ProtoFunction -import org.opentaint.ir.go.proto.ProtoFunctionBody -import org.opentaint.ir.go.proto.ProtoGlobal -import org.opentaint.ir.go.proto.ProtoModelProgram -import org.opentaint.ir.go.proto.ProtoNamedType -import org.opentaint.ir.go.proto.ProtoNamedTypeKind -import org.opentaint.ir.go.proto.ProtoPackage -import org.opentaint.ir.go.proto.ProtoProgram -import org.opentaint.ir.go.proto.ProtoStructField -import org.opentaint.ir.go.proto.ProtoStructType -import org.opentaint.ir.go.proto.ProtoTypeDefinition - -internal class GoModelMerger { - fun merge(base: ProtoProgram, models: List): ProtoProgram { - val state = ModelMergeState() - return models.fold(base) { program, model -> - mergeModel(program, model.program, state, model.source) - } - } - - private fun mergeModel( - base: ProtoProgram, - rawModel: ProtoProgram, - state: ModelMergeState, - source: String, - ): ProtoProgram { - val normalizer = try { - ModelPathNormalizer.create(rawModel) - } catch (error: IllegalArgumentException) { - throw IllegalArgumentException("invalid Go model $source: ${error.message}", error) - } - normalizer.replacements.values.forEach { target -> - val previous = state.modeledPackages.putIfAbsent(target, source) - require(previous == null) { - "Go package \"$target\" is modeled more than once ($previous and $source)" - } - } - - var model = rawModel - var baseIndex = ProtoIndex(base) - var modelIndex = ProtoIndex(model, normalizer) - val fieldPlans = invalidModel(source) { - planModelStructFields(baseIndex, modelIndex, normalizer) - } - model = applyModelStructFieldPlans(model, fieldPlans) - modelIndex = ProtoIndex(model, normalizer) - model = invalidModel(source) { - remapModelStructFieldAccesses(model, modelIndex, fieldPlans) - } - modelIndex = ProtoIndex(model, normalizer) - - val ownedOldPackages = model.packagesList - .filter { it.importPath in normalizer.replacements } - .mapTo(mutableSetOf()) { it.id } - val importedTargetPackages = model.packagesList - .filter { it.id !in ownedOldPackages && it.importPath in normalizer.targets } - .mapTo(mutableSetOf()) { it.id } - val importedTargetFunctions = modelIndex.functions.values - .filter { it.packageId in importedTargetPackages } - .mapTo(mutableSetOf()) { it.id } - val initOldFunctions = collectInitializerFunctions(model, ownedOldPackages) - - val basePackages = baseIndex.reversePackageKeys() - model.packagesList.forEach { modelPackage -> - val target = normalizer.replacements[modelPackage.importPath] ?: return@forEach - val targetPackage = basePackages[target]?.let(baseIndex.packages::get) ?: return@forEach - require(modelPackage.name == targetPackage.name) { - "Go model package \"${modelPackage.importPath}\" has package name " + - "\"${modelPackage.name}\", want \"${targetPackage.name}\" " + - "(the modeled package name)" - } - } - - val counters = IdCounters.forProgram(base) - val modelFunctionKeys = modelIndex.functionKeysById().toMutableMap() - modelIndex.functions.forEach { (id, function) -> - if (function.parentFunctionId != 0) { - modelFunctionKeys[id] = modelFunctionKeys.getValue(id) + - "#opentaint-model-anonymous:$id" - } - } - val remap = IdRemap( - types = assignIds(modelIndex.typeKeysById(), baseIndex.reverseTypeKeys(), counters::nextType), - packages = assignIds( - modelIndex.packageKeys, - baseIndex.reversePackageKeys(), - counters::nextPackage, - ), - functions = assignIds( - modelFunctionKeys, - baseIndex.reverseFunctionKeys(), - counters::nextFunction, - ), - named = assignIds(modelIndex.namedKeys, baseIndex.reverseNamedKeys(), counters::nextNamed), - globals = assignIds( - modelIndex.globalKeysById(), - baseIndex.reverseGlobalKeys(), - counters::nextGlobal, - ), - consts = assignIds( - modelIndex.constKeysById(), - baseIndex.reverseConstKeys(), - counters::nextConst, - ), - ) - - validateFunctionSignatures( - baseIndex, - modelIndex, - normalizer, - modelFunctionKeys, - initOldFunctions, - ) - - model = normalizeModel(model, normalizer, ownedOldPackages) - model = model.toBuilder() - .clearFunctionBodies() - .addAllFunctionBodies( - model.functionBodiesList.filterNot { it.functionId in importedTargetFunctions }, - ) - .clearPackages() - .addAllPackages(model.packagesList.map { pkg -> - if (pkg.id !in importedTargetPackages) { - pkg - } else { - pkg.toBuilder() - .setInitFunctionId(0) - .clearFunctions() - .clearGlobals() - .clearConstants() - .clearNamedTypes() - .addAllNamedTypes(pkg.namedTypesList.map { named -> - named.toBuilder() - .clearMethodIds() - .clearPointerMethodIds() - .build() - }) - .build() - } - }) - .build() - val ownedPackages = ownedOldPackages.mapTo(mutableSetOf()) { remap.packages.getValue(it) } - val initFunctions = initOldFunctions.mapTo(mutableSetOf()) { remap.functions.getValue(it) } - model = try { - remapMessageIds(model, remap) as ProtoProgram - } catch (error: IllegalArgumentException) { - throw IllegalArgumentException("merging Go model $source: ${error.message}", error) - } - - var result = mergeModelStructFields(base, model, fieldPlans, remap, counters, source) - baseIndex = ProtoIndex(result) - - val missingTypes = model.typesList.filter { it.id !in baseIndex.types } - if (missingTypes.isNotEmpty()) { - result = result.toBuilder().addAllTypes(missingTypes).build() - } - - result = mergePackages(result, model, ownedPackages, initFunctions) - baseIndex = ProtoIndex(result) - result = mergeFunctionBodies(result, model, baseIndex, ownedPackages, initFunctions, state, source) - result = mergeGenericInstanceBodies(result, ownedPackages, state, source, counters) - validateReferences(result, source) - return result - } - - private inline fun invalidModel(source: String, action: () -> T): T { - return try { - action() - } catch (error: IllegalArgumentException) { - throw IllegalArgumentException("invalid Go model $source: ${error.message}", error) - } - } - - private fun collectInitializerFunctions( - model: ProtoProgram, - ownedPackages: Set, - ): MutableSet { - val functions = model.packagesList - .filter { it.id in ownedPackages } - .flatMap { it.functionsList } - val result = functions - .filter { it.name == "init" || it.name.startsWith("init#") } - .mapTo(mutableSetOf()) { it.id } - - do { - val size = result.size - functions.forEach { function -> - if (function.parentFunctionId in result) { - result += function.id - } - } - } while (result.size != size) - return result - } - - private fun validateFunctionSignatures( - baseIndex: ProtoIndex, - modelIndex: ProtoIndex, - normalizer: ModelPathNormalizer, - modelFunctionKeys: Map, - initFunctions: Set, - ) { - val baseFunctions = baseIndex.reverseFunctionKeys() - modelFunctionKeys.forEach { (modelId, key) -> - val baseId = baseFunctions[key] ?: return@forEach - val modelFunction = modelIndex.functions[modelId] ?: return@forEach - if ( - modelFunction.parentFunctionId != 0 || - modelId in initFunctions || - !modelIndex.isOwnedPackage(normalizer, modelFunction.packageId) - ) { - return@forEach - } - val baseFunction = baseIndex.functions.getValue(baseId) - require( - modelIndex.typeKey(modelFunction.signatureTypeId) == - baseIndex.typeKey(baseFunction.signatureTypeId) && - modelFunction.isMethod == baseFunction.isMethod && - modelFunction.isPointerReceiver == baseFunction.isPointerReceiver, - ) { - "Go model function $key has a different signature from its target" - } - } - } - - private fun normalizeModel( - model: ProtoProgram, - normalizer: ModelPathNormalizer, - ownedPackages: Set, - ): ProtoProgram { - val packages = model.packagesList.map { pkg -> - val builder = pkg.toBuilder() - if (pkg.id in ownedPackages) { - builder - .setInitFunctionId(0) - .setImportPath(normalizer.path(pkg.importPath)) - .setIsDependency(true) - .setIsStdlib(false) - } - builder - .clearNamedTypes() - .addAllNamedTypes(pkg.namedTypesList.map { named -> - named.toBuilder().setFullName(normalizer.symbol(named.fullName)).build() - }) - .clearFunctions() - .addAllFunctions(pkg.functionsList.map { function -> - function.toBuilder().setFullName(normalizer.symbol(function.fullName)).build() - }) - .clearGlobals() - .addAllGlobals(pkg.globalsList.map { global -> - global.toBuilder().setFullName(normalizer.symbol(global.fullName)).build() - }) - .clearConstants() - .addAllConstants(pkg.constantsList.map { constant -> - constant.toBuilder().setFullName(normalizer.symbol(constant.fullName)).build() - }) - .build() - } - return model.toBuilder().clearPackages().addAllPackages(packages).build() - } - - private fun mergeModelStructFields( - base: ProtoProgram, - model: ProtoProgram, - plans: Map, - remap: IdRemap, - counters: IdCounters, - source: String, - ): ProtoProgram { - var types = base.typesList - var packages = base.packagesList - val modelIndex = ProtoIndex(model) - - plans.toSortedMap().forEach { (modelNamedId, plan) -> - if (plan.extraFieldNames.isEmpty()) { - return@forEach - } - val remappedNamedId = remap.named.getValue(modelNamedId) - val modelNamed = modelIndex.named[remappedNamedId] - ?: throw IllegalArgumentException( - "merging Go model $source: model named type $remappedNamedId is missing", - ) - val extraFields = modelNamed.fieldsList.filter { it.name in plan.extraFieldNames } - val baseIndex = ProtoIndex( - base.toBuilder().clearTypes().addAllTypes(types).clearPackages().addAllPackages(packages).build(), - ) - val targetNamed = baseIndex.named[plan.targetNamedId] - ?: throw IllegalArgumentException( - "merging Go model $source: Go model target named type " + - "${plan.targetNamedId} is missing", - ) - val underlying = baseIndex.types[targetNamed.underlyingTypeId] - ?.takeIf { it.typeCase == ProtoTypeDefinition.TypeCase.STRUCT_TYPE } - ?.structType - ?: throw IllegalArgumentException( - "merging Go model $source: Go model target type " + - "${targetNamed.fullName} has no struct body", - ) - - val combinedFields = underlying.fieldsList.toMutableList() - combinedFields += extraFields.map(::fieldDeclarationToStructField) - val combined = ProtoTypeDefinition.newBuilder() - .setId(counters.nextType()) - .setStructType(ProtoStructType.newBuilder().addAllFields(combinedFields)) - .build() - types = types + combined - packages = packages.map { pkg -> - if (pkg.namedTypesList.none { it.id == targetNamed.id }) { - pkg - } else { - pkg.toBuilder() - .clearNamedTypes() - .addAllNamedTypes(pkg.namedTypesList.map { named -> - if (named.id != targetNamed.id) { - named - } else { - named.toBuilder() - .addAllFields(extraFields) - .setUnderlyingTypeId(combined.id) - .build() - } - }) - .build() - } - } - } - return base.toBuilder().clearTypes().addAllTypes(types).clearPackages().addAllPackages(packages).build() - } - - private fun mergePackages( - base: ProtoProgram, - model: ProtoProgram, - ownedPackages: Set, - initFunctions: Set, - ): ProtoProgram { - val packages = base.packagesList.toMutableList() - val packageIndices = packages.mapIndexed { index, pkg -> pkg.id to index }.toMap().toMutableMap() - - model.packagesList.forEach { modelPackage -> - val basePosition = packageIndices[modelPackage.id] - if (basePosition == null) { - val added = if (modelPackage.id in ownedPackages) { - modelPackage.toBuilder() - .clearFunctions() - .addAllFunctions( - modelPackage.functionsList - .filterNot { it.id in initFunctions } - .map(::asModelSupport), - ) - .build() - } else { - modelPackage - } - packageIndices[added.id] = packages.size - packages += added - return@forEach - } - - val basePackage = packages[basePosition] - val named = basePackage.namedTypesList.toMutableList() - val namedIndices = named.mapIndexed { index, value -> value.id to index }.toMap().toMutableMap() - modelPackage.namedTypesList.forEach { modelNamed -> - val position = namedIndices[modelNamed.id] - if (position == null) { - namedIndices[modelNamed.id] = named.size - named += modelNamed - } else { - val current = named[position] - named[position] = current.toBuilder() - .clearMethodIds() - .addAllMethodIds(appendUniqueIds(current.methodIdsList, modelNamed.methodIdsList)) - .clearPointerMethodIds() - .addAllPointerMethodIds( - appendUniqueIds( - current.pointerMethodIdsList, - modelNamed.pointerMethodIdsList, - ), - ) - .build() - } - } - - val functions = basePackage.functionsList.toMutableList() - val functionIndices = functions - .mapIndexed { index, value -> value.id to index } - .toMap() - .toMutableMap() - modelPackage.functionsList.forEach { modelFunction -> - if (modelFunction.id in initFunctions) { - return@forEach - } - val position = functionIndices[modelFunction.id] - if (position == null) { - functionIndices[modelFunction.id] = functions.size - functions += if (modelPackage.id in ownedPackages) { - asModelSupport(modelFunction) - } else { - modelFunction - } - } else if (modelPackage.id in ownedPackages) { - functions[position] = functions[position].toBuilder() - .clearAnonFunctionIds() - .addAllAnonFunctionIds(modelFunction.anonFunctionIdsList) - .build() - } - } - - packages[basePosition] = basePackage.toBuilder() - .clearImportIds() - .addAllImportIds(appendUniqueIds(basePackage.importIdsList, modelPackage.importIdsList)) - .clearNamedTypes() - .addAllNamedTypes(named) - .clearFunctions() - .addAllFunctions(functions) - .clearGlobals() - .addAllGlobals(mergeById(basePackage.globalsList, modelPackage.globalsList) { it.id }) - .clearConstants() - .addAllConstants(mergeById(basePackage.constantsList, modelPackage.constantsList) { it.id }) - .build() - } - - return base.toBuilder().clearPackages().addAllPackages(packages).build() - } - - private fun mergeFunctionBodies( - base: ProtoProgram, - model: ProtoProgram, - baseIndex: ProtoIndex, - ownedPackages: Set, - initFunctions: Set, - state: ModelMergeState, - source: String, - ): ProtoProgram { - val bodies = base.functionBodiesList.toMutableList() - val bodyIndices = bodies.mapIndexed { index, body -> body.functionId to index }.toMap().toMutableMap() - val functionsWithBodies = mutableSetOf() - - model.functionBodiesList.forEach { body -> - if (body.functionId in initFunctions) { - return@forEach - } - val function = baseIndex.functions[body.functionId] ?: return@forEach - if (function.packageId !in ownedPackages) { - return@forEach - } - val previous = state.modeledFunctions.putIfAbsent(body.functionId, source) - require(previous == null) { - "Go function \"${function.fullName}\" is modeled more than once ($previous and $source)" - } - val position = bodyIndices[body.functionId] - if (position == null) { - bodyIndices[body.functionId] = bodies.size - bodies += body - } else { - bodies[position] = body - } - functionsWithBodies += body.functionId - } - - val packages = base.packagesList.map { pkg -> - if (pkg.functionsList.none { it.id in functionsWithBodies }) { - pkg - } else { - pkg.toBuilder() - .clearFunctions() - .addAllFunctions(pkg.functionsList.map { function -> - if (function.id in functionsWithBodies) { - function.toBuilder() - .setHasBody(true) - .setIsSynthetic(true) - .setSyntheticKind( - if (function.syntheticKind == MODEL_SUPPORT_KIND) { - MODEL_SUPPORT_KIND - } else { - MODEL_KIND - }, - ) - .build() - } else { - function - } - }) - .build() - } - } - return base.toBuilder() - .clearPackages() - .addAllPackages(packages) - .clearFunctionBodies() - .addAllFunctionBodies(bodies) - .build() - } - - private fun mergeGenericInstanceBodies( - program: ProtoProgram, - ownedPackages: Set, - state: ModelMergeState, - source: String, - counters: IdCounters, - ): ProtoProgram { - val index = ProtoIndex(program) - val genericOriginIds = index.functions.values - .mapNotNullTo(mutableSetOf()) { it.originFunctionId.takeIf { id -> id != 0 } } - val modeledGenericBodies = program.functionBodiesList.mapNotNull { body -> - val function = index.functions[body.functionId] ?: return@mapNotNull null - if ( - function.packageId !in ownedPackages || - function.id !in genericOriginIds || - state.modeledFunctions[function.id] != source || - function.syntheticKind != MODEL_KIND - ) { - return@mapNotNull null - } - function.id to ModeledGenericBody(function, body) - }.toMap() - if (modeledGenericBodies.isEmpty()) { - return program - } - - val bodies = program.functionBodiesList.toMutableList() - val bodyIndices = bodies.mapIndexed { position, body -> body.functionId to position } - .toMap() - .toMutableMap() - val bodiesByFunction = program.functionBodiesList.associateBy { it.functionId } - val modeledInstances = mutableSetOf() - val instanceAnonymousFunctions = mutableMapOf>() - val addedFunctions = mutableListOf() - val types = program.typesList.toMutableList() - index.functions.values.forEach { function -> - val modeled = modeledGenericBodies[function.originFunctionId] ?: return@forEach - val previous = state.modeledFunctions.putIfAbsent(function.id, source) - require(previous == null) { - "Go function \"${function.fullName}\" is modeled more than once ($previous and $source)" - } - val typeArgumentsByIndex = modeled.function.typeParamsList.associate { typeParam -> - require(typeParam.index in function.typeArgIdsList.indices) { - "Go generic instance ${function.fullName} has no type argument for " + - "parameter ${typeParam.name}[${typeParam.index}]" - } - typeParam.index to function.typeArgIdsList[typeParam.index] - } - val specializer = GenericTypeSpecializer(index.types, typeArgumentsByIndex, counters) - val anonymousIds = collectAnonymousFunctionIds(modeled.function, index.functions) - val functionIds = anonymousIds.associateWith { counters.nextFunction() }.toMutableMap() - functionIds[modeled.function.id] = function.id - val functionRemap = { id: Int -> functionIds[id] ?: id } - val body = specializer.specialize(modeled.body, functionRemap) - anonymousIds.forEach { anonymousId -> - val original = index.functions.getValue(anonymousId) - val suffix = function.name.removePrefix(modeled.function.name) - .ifEmpty { "#${function.id}" } - val specializedFunction = specializer.specialize(original, functionRemap) - .toBuilder() - .setName(original.name + suffix) - .setFullName(original.fullName + suffix) - .setIsSynthetic(true) - .setSyntheticKind(MODEL_SUPPORT_KIND) - .build() - addedFunctions += specializedFunction - - val originalBody = bodiesByFunction[anonymousId] - require(originalBody != null) { - "Go model anonymous function ${original.fullName} has no body" - } - val specializedBody = specializer.specialize(originalBody, functionRemap) - val anonymousBodyPosition = bodyIndices[specializedBody.functionId] - if (anonymousBodyPosition == null) { - bodyIndices[specializedBody.functionId] = bodies.size - bodies += specializedBody - } else { - bodies[anonymousBodyPosition] = specializedBody - } - } - instanceAnonymousFunctions[function.id] = modeled.function.anonFunctionIdsList.map(functionRemap) - types += specializer.addedTypes - val position = bodyIndices[function.id] - if (position == null) { - bodyIndices[function.id] = bodies.size - bodies += body - } else { - bodies[position] = body - } - modeledInstances += function.id - } - if (modeledInstances.isEmpty()) { - return program - } - - val packages = program.packagesList.map { pkg -> - val packageAdditions = addedFunctions.filter { it.packageId == pkg.id } - if (pkg.functionsList.none { it.id in modeledInstances } && packageAdditions.isEmpty()) { - pkg - } else { - pkg.toBuilder() - .clearFunctions() - .addAllFunctions(pkg.functionsList.map { function -> - if (function.id !in modeledInstances) { - function - } else { - function.toBuilder() - .setHasBody(true) - .setIsSynthetic(true) - .setSyntheticKind(MODEL_KIND) - .clearAnonFunctionIds() - .addAllAnonFunctionIds(instanceAnonymousFunctions.getValue(function.id)) - .build() - } - }) - .addAllFunctions(packageAdditions) - .build() - } - } - return program.toBuilder() - .clearTypes() - .addAllTypes(types) - .clearPackages() - .addAllPackages(packages) - .clearFunctionBodies() - .addAllFunctionBodies(bodies) - .build() - } - - private fun asModelSupport(function: ProtoFunction): ProtoFunction { - return function.toBuilder() - .setIsSynthetic(true) - .setSyntheticKind(MODEL_SUPPORT_KIND) - .build() - } - - private data class ModeledGenericBody( - val function: ProtoFunction, - val body: ProtoFunctionBody, - ) - - private fun collectAnonymousFunctionIds( - root: ProtoFunction, - functions: Map, - ): List { - val result = linkedSetOf() - val pending = ArrayDeque(root.anonFunctionIdsList) - while (pending.isNotEmpty()) { - val id = pending.removeFirst() - if (!result.add(id)) { - continue - } - val function = functions[id] - ?: throw IllegalArgumentException("Go model anonymous function $id is missing") - pending.addAll(function.anonFunctionIdsList) - } - return result.toList() - } -} - -private class ModelMergeState { - val modeledPackages = mutableMapOf() - val modeledFunctions = mutableMapOf() -} - -private class GenericTypeSpecializer( - private val definitions: Map, - private val typeArgumentsByIndex: Map, - private val counters: IdCounters, -) { - private val specialized = mutableMapOf() - private val active = mutableSetOf() - val addedTypes = mutableListOf() - - fun specialize(body: ProtoFunctionBody, remapFunction: (Int) -> Int): ProtoFunctionBody = - remapMessageIdsOfClass( - remapMessageIdsOfClass(body, MapName.TYPE, ::specializeType), - MapName.FUNCTION, - remapFunction, - ) as ProtoFunctionBody - - fun specialize(function: ProtoFunction, remapFunction: (Int) -> Int): ProtoFunction = - remapMessageIdsOfClass( - remapMessageIdsOfClass(function, MapName.TYPE, ::specializeType), - MapName.FUNCTION, - remapFunction, - ) as ProtoFunction - - private fun specializeType(id: Int): Int { - if (id == 0) { - return 0 - } - specialized[id]?.let { return it } - val definition = definitions[id] - ?: throw IllegalArgumentException("Go model type $id is missing during generic specialization") - if (definition.typeCase == ProtoTypeDefinition.TypeCase.TYPE_PARAM) { - typeArgumentsByIndex[definition.typeParam.index]?.let { typeArgument -> - specialized[id] = typeArgument - return typeArgument - } - } - require(active.add(id)) { "recursive Go model type $id cannot be specialized" } - val rewritten = rewrite(definition) - active -= id - if (rewritten == definition) { - specialized[id] = id - return id - } - val result = rewritten.toBuilder().setId(counters.nextType()).build() - addedTypes += result - specialized[id] = result.id - return result.id - } - - private fun rewrite(definition: ProtoTypeDefinition): ProtoTypeDefinition { - val builder = definition.toBuilder() - when (definition.typeCase) { - ProtoTypeDefinition.TypeCase.POINTER -> builder.setPointer( - definition.pointer.toBuilder().setElemTypeId(specializeType(definition.pointer.elemTypeId)), - ) - ProtoTypeDefinition.TypeCase.ARRAY -> builder.setArray( - definition.array.toBuilder().setElemTypeId(specializeType(definition.array.elemTypeId)), - ) - ProtoTypeDefinition.TypeCase.SLICE -> builder.setSlice( - definition.slice.toBuilder().setElemTypeId(specializeType(definition.slice.elemTypeId)), - ) - ProtoTypeDefinition.TypeCase.MAP_TYPE -> builder.setMapType( - definition.mapType.toBuilder() - .setKeyTypeId(specializeType(definition.mapType.keyTypeId)) - .setValueTypeId(specializeType(definition.mapType.valueTypeId)), - ) - ProtoTypeDefinition.TypeCase.CHAN_TYPE -> builder.setChanType( - definition.chanType.toBuilder().setElemTypeId(specializeType(definition.chanType.elemTypeId)), - ) - ProtoTypeDefinition.TypeCase.STRUCT_TYPE -> builder.setStructType( - definition.structType.toBuilder() - .clearFields() - .addAllFields(definition.structType.fieldsList.map { field -> - field.toBuilder().setTypeId(specializeType(field.typeId)).build() - }), - ) - ProtoTypeDefinition.TypeCase.INTERFACE_TYPE -> builder.setInterfaceType( - definition.interfaceType.toBuilder() - .clearMethods() - .addAllMethods(definition.interfaceType.methodsList.map { method -> - method.toBuilder() - .setSignatureTypeId(specializeType(method.signatureTypeId)) - .build() - }) - .clearEmbedTypeIds() - .addAllEmbedTypeIds(definition.interfaceType.embedTypeIdsList.map(::specializeType)), - ) - ProtoTypeDefinition.TypeCase.FUNC_TYPE -> builder.setFuncType( - definition.funcType.toBuilder() - .clearParamTypeIds() - .addAllParamTypeIds(definition.funcType.paramTypeIdsList.map(::specializeType)) - .clearResultTypeIds() - .addAllResultTypeIds(definition.funcType.resultTypeIdsList.map(::specializeType)) - .setRecvTypeId(specializeType(definition.funcType.recvTypeId)), - ) - ProtoTypeDefinition.TypeCase.NAMED_REF -> builder.setNamedRef( - definition.namedRef.toBuilder() - .clearTypeArgIds() - .addAllTypeArgIds(definition.namedRef.typeArgIdsList.map(::specializeType)), - ) - ProtoTypeDefinition.TypeCase.TYPE_PARAM -> {} - ProtoTypeDefinition.TypeCase.TUPLE -> builder.setTuple( - definition.tuple.toBuilder() - .clearElementTypeIds() - .addAllElementTypeIds(definition.tuple.elementTypeIdsList.map(::specializeType)), - ) - ProtoTypeDefinition.TypeCase.BASIC, - ProtoTypeDefinition.TypeCase.UNSAFE_POINTER, - ProtoTypeDefinition.TypeCase.TYPE_NOT_SET, - -> {} - } - return builder.build() - } -} - -private data class ModelPathNormalizer( - val replacements: Map, - val targets: Set, - private val orderedModelPaths: List, -) { - fun path(value: String): String = replacements[value] ?: value - - fun symbol(value: String): String { - return orderedModelPaths.fold(value) { result, modelPath -> - result.replace(modelPath, replacements.getValue(modelPath)) - } - } - - companion object { - fun create(program: ProtoProgram): ModelPathNormalizer { - val replacements = program.packagesList - .filterNot { it.isDependency || it.isStdlib || it.importPath == UNIVERSE_PACKAGE_PATH } - .associate { pkg -> - require(pkg.importPath.startsWith(GO_MODEL_PREFIX)) { - "Go model package \"${pkg.importPath}\" is not model-addressed, " + - "its import path must be ${GO_MODEL_PREFIX}" - } - val target = pkg.importPath.removePrefix(GO_MODEL_PREFIX) - require(target.isNotEmpty()) { - "Go model package \"${pkg.importPath}\" has an empty target package" - } - pkg.importPath to target - } - require(replacements.isNotEmpty()) { - "Go model contains no model-addressed packages, expected at least one package " + - "under $GO_MODEL_PREFIX" - } - return ModelPathNormalizer( - replacements, - replacements.values.toSet(), - replacements.keys.sortedByDescending(String::length), - ) - } - } -} - -private class ProtoIndex( - val program: ProtoProgram, - private val normalizer: ModelPathNormalizer? = null, -) { - val packages = program.packagesList.associateBy { it.id } - val types = program.typesList.associateBy { it.id } - val named = program.packagesList.flatMap { it.namedTypesList }.associateBy { it.id } - val functions = program.packagesList.flatMap { it.functionsList }.associateBy { it.id } - val globals = program.packagesList.flatMap { it.globalsList }.associateBy { it.id } - val consts = program.packagesList.flatMap { it.constantsList }.associateBy { it.id } - val packageKeys = program.packagesList.associate { it.id to path(it.importPath) } - val namedKeys = program.packagesList.flatMap { pkg -> - pkg.namedTypesList.map { named -> named.id to "${path(pkg.importPath)}.${named.name}" } - }.toMap() - - private val typeMemo = mutableMapOf() - private val activeTypes = mutableSetOf() - - fun typeKey(id: Int): String { - if (id == 0) { - return "void" - } - typeMemo[id]?.let { return it } - if (!activeTypes.add(id)) { - return "" - } - val definition = types[id] - val key = if (definition == null) { - "" - } else { - when (definition.typeCase) { - ProtoTypeDefinition.TypeCase.BASIC -> "basic:${definition.basic.kind.number}" - ProtoTypeDefinition.TypeCase.POINTER -> "pointer:${typeKey(definition.pointer.elemTypeId)}" - ProtoTypeDefinition.TypeCase.ARRAY -> - "array:${definition.array.length}:${typeKey(definition.array.elemTypeId)}" - ProtoTypeDefinition.TypeCase.SLICE -> "slice:${typeKey(definition.slice.elemTypeId)}" - ProtoTypeDefinition.TypeCase.MAP_TYPE -> - "map:${typeKey(definition.mapType.keyTypeId)}:" + - typeKey(definition.mapType.valueTypeId) - ProtoTypeDefinition.TypeCase.CHAN_TYPE -> - "chan:${definition.chanType.direction.number}:" + - typeKey(definition.chanType.elemTypeId) - ProtoTypeDefinition.TypeCase.STRUCT_TYPE -> buildString { - append("struct{") - definition.structType.fieldsList.forEach { field -> - append(field.name) - append(':') - append(typeKey(field.typeId)) - append(':') - append(field.embedded) - append(':') - append(field.exported) - append(':') - append(field.tag) - append('|') - } - append('}') - } - ProtoTypeDefinition.TypeCase.INTERFACE_TYPE -> buildString { - append("interface{") - definition.interfaceType.methodsList.forEach { method -> - append(method.name) - append(':') - append(typeKey(method.signatureTypeId)) - append('|') - } - definition.interfaceType.embedTypeIdsList.forEach { embedded -> - append("embed:") - append(typeKey(embedded)) - append('|') - } - append('}') - } - ProtoTypeDefinition.TypeCase.FUNC_TYPE -> buildString { - append("func(") - definition.funcType.paramTypeIdsList.forEach { parameter -> - append(typeKey(parameter)) - append(',') - } - append(")->(") - definition.funcType.resultTypeIdsList.forEach { result -> - append(typeKey(result)) - append(',') - } - append("):variadic=") - append(definition.funcType.variadic) - append(":recv=") - append(typeKey(definition.funcType.recvTypeId)) - } - ProtoTypeDefinition.TypeCase.NAMED_REF -> buildString { - append("named:") - append(namedKeys[definition.namedRef.namedTypeId]) - append('[') - definition.namedRef.typeArgIdsList.forEach { argument -> - append(typeKey(argument)) - append(',') - } - append(']') - } - ProtoTypeDefinition.TypeCase.TYPE_PARAM -> - "typeparam:${definition.typeParam.name}:${definition.typeParam.index}:" + - typeKey(definition.typeParam.constraintTypeId) - ProtoTypeDefinition.TypeCase.TUPLE -> buildString { - append("tuple(") - definition.tuple.elementTypeIdsList.forEach { element -> - append(typeKey(element)) - append(',') - } - append(')') - } - ProtoTypeDefinition.TypeCase.UNSAFE_POINTER -> "unsafe.Pointer" - ProtoTypeDefinition.TypeCase.TYPE_NOT_SET -> "" - } - } - activeTypes -= id - typeMemo[id] = key - return key - } - - fun reversePackageKeys(): MutableMap = packageKeys.reverse() - fun reverseNamedKeys(): MutableMap = namedKeys.reverse() - fun reverseTypeKeys(): MutableMap = typeKeysById().reverse() - fun reverseFunctionKeys(): MutableMap = functionKeysById().reverse() - fun reverseGlobalKeys(): MutableMap = globalKeysById().reverse() - fun reverseConstKeys(): MutableMap = constKeysById().reverse() - fun typeKeysById(): Map = types.keys.associateWith(::typeKey) - fun functionKeysById(): Map = functions.mapValues { symbol(it.value.fullName) } - fun globalKeysById(): Map = globals.mapValues { symbol(it.value.fullName) } - fun constKeysById(): Map = consts.mapValues { symbol(it.value.fullName) } - - fun isOwnedPackage(normalizer: ModelPathNormalizer, packageId: Int): Boolean { - val pkg = packages[packageId] ?: return false - return pkg.importPath in normalizer.replacements - } - - private fun path(value: String): String = normalizer?.path(value) ?: value - private fun symbol(value: String): String = normalizer?.symbol(value) ?: value -} - -private data class IdRemap( - val types: Map, - val packages: Map, - val functions: Map, - val named: Map, - val globals: Map, - val consts: Map, -) { - fun values(name: MapName): Map = when (name) { - MapName.TYPE -> types - MapName.PACKAGE -> packages - MapName.FUNCTION -> functions - MapName.NAMED -> named - MapName.GLOBAL -> globals - MapName.CONST -> consts - MapName.NONE -> emptyMap() - } -} - -private class IdCounters( - private var typeId: Int, - private var packageId: Int, - private var functionId: Int, - private var namedId: Int, - private var globalId: Int, - private var constId: Int, -) { - fun nextType(): Int = ++typeId - fun nextPackage(): Int = ++packageId - fun nextFunction(): Int = ++functionId - fun nextNamed(): Int = ++namedId - fun nextGlobal(): Int = ++globalId - fun nextConst(): Int = ++constId - - companion object { - fun forProgram(program: ProtoProgram): IdCounters = IdCounters( - typeId = program.typesList.maxOfOrNull { it.id } ?: 0, - packageId = program.packagesList.maxOfOrNull { it.id } ?: 0, - functionId = program.packagesList.flatMap { it.functionsList }.maxOfOrNull { it.id } ?: 0, - namedId = program.packagesList.flatMap { it.namedTypesList }.maxOfOrNull { it.id } ?: 0, - globalId = program.packagesList.flatMap { it.globalsList }.maxOfOrNull { it.id } ?: 0, - constId = program.packagesList.flatMap { it.constantsList }.maxOfOrNull { it.id } ?: 0, - ) - } -} - -private data class ModelStructFieldPlan( - val targetNamedId: Int, - val indexByModel: Map, - val extraFieldNames: Set, -) - -private fun planModelStructFields( - baseIndex: ProtoIndex, - modelIndex: ProtoIndex, - normalizer: ModelPathNormalizer, -): Map { - val plans = mutableMapOf() - val baseNamed = baseIndex.reverseNamedKeys() - modelIndex.program.packagesList.forEach { pkg -> - if (pkg.importPath !in normalizer.replacements) { - return@forEach - } - pkg.namedTypesList.forEach { modelNamed -> - val targetId = baseNamed[modelIndex.namedKeys.getValue(modelNamed.id)] ?: return@forEach - val targetNamed = baseIndex.named.getValue(targetId) - if (modelNamed.kind != ProtoNamedTypeKind.NAMED_TYPE_STRUCT) { - return@forEach - } - require(targetNamed.kind == ProtoNamedTypeKind.NAMED_TYPE_STRUCT) { - "Go model type ${modelIndex.namedKeys.getValue(modelNamed.id)} is a struct, " + - "but its target is not a struct" - } - val targetFields = targetNamed.fieldsList.associateBy { it.name } - var nextIndex = targetNamed.fieldsList.maxOfOrNull { it.index + 1 } ?: 0 - val indexByModel = mutableMapOf() - val extraFieldNames = mutableSetOf() - modelNamed.fieldsList.forEach { modelField -> - val targetField = targetFields[modelField.name] - if (targetField == null) { - indexByModel[modelField.index] = nextIndex++ - extraFieldNames += modelField.name - } else { - require( - modelIndex.typeKey(modelField.typeId) == baseIndex.typeKey(targetField.typeId), - ) { - "Go model field ${modelIndex.namedKeys.getValue(modelNamed.id)}." + - "${modelField.name} has a different type from its target" - } - indexByModel[modelField.index] = targetField.index - } - } - plans[modelNamed.id] = ModelStructFieldPlan(targetId, indexByModel, extraFieldNames) - } - } - return plans -} - -private fun applyModelStructFieldPlans( - model: ProtoProgram, - plans: Map, -): ProtoProgram { - val underlyingPlans = model.packagesList - .flatMap { it.namedTypesList } - .mapNotNull { named -> plans[named.id]?.let { named.underlyingTypeId to it } } - .toMap() - val types = model.typesList.map { type -> - val plan = underlyingPlans[type.id] - if (plan == null || type.typeCase != ProtoTypeDefinition.TypeCase.STRUCT_TYPE) { - type - } else { - val fields = type.structType.fieldsList.map { field -> - val index = plan.indexByModel[field.index] ?: field.index - field.toBuilder().setIndex(index).build() - } - type.toBuilder().setStructType(type.structType.toBuilder().clearFields().addAllFields(fields)).build() - } - } - val packages = model.packagesList.map { pkg -> - pkg.toBuilder() - .clearNamedTypes() - .addAllNamedTypes(pkg.namedTypesList.map { named -> - val plan = plans[named.id] ?: return@map named - named.toBuilder() - .clearFields() - .addAllFields(named.fieldsList.map { field -> - field.toBuilder() - .setIndex(plan.indexByModel.getValue(field.index)) - .build() - }) - .build() - }) - .build() - } - return model.toBuilder().clearTypes().addAllTypes(types).clearPackages().addAllPackages(packages).build() -} - -private fun remapModelStructFieldAccesses( - model: ProtoProgram, - modelIndex: ProtoIndex, - plans: Map, -): ProtoProgram { - val underlyingToNamed = modelIndex.named.values.associate { it.underlyingTypeId to it.id } - - fun remap(typeId: Int, fieldIndex: Int, fieldName: String): Int { - val namedId = modelNamedTypeForFieldAccess(modelIndex, underlyingToNamed, typeId, mutableSetOf()) - val plan = plans[namedId] ?: return fieldIndex - return plan.indexByModel[fieldIndex] - ?: throw IllegalArgumentException( - "Go model field access $fieldName[$fieldIndex] has no target field mapping", - ) - } - - val bodies = model.functionBodiesList.map { body -> - body.toBuilder() - .clearBlocks() - .addAllBlocks(body.blocksList.map { block -> - block.toBuilder() - .clearInstructions() - .addAllInstructions(block.instructionsList.map { instruction -> - when (instruction.instCase) { - org.opentaint.ir.go.proto.ProtoInstruction.InstCase.FIELD_ADDR -> { - val access = instruction.fieldAddr - instruction.toBuilder().setFieldAddr( - access.toBuilder().setFieldIndex( - remap(access.x.typeId, access.fieldIndex, access.fieldName), - ), - ).build() - } - org.opentaint.ir.go.proto.ProtoInstruction.InstCase.FIELD -> { - val access = instruction.field - instruction.toBuilder().setField( - access.toBuilder().setFieldIndex( - remap(access.x.typeId, access.fieldIndex, access.fieldName), - ), - ).build() - } - else -> instruction - } - }) - .build() - }) - .build() - } - return model.toBuilder().clearFunctionBodies().addAllFunctionBodies(bodies).build() -} - -private fun modelNamedTypeForFieldAccess( - index: ProtoIndex, - underlyingToNamed: Map, - typeId: Int, - active: MutableSet, -): Int? { - if (typeId == 0 || !active.add(typeId)) { - return null - } - val type = index.types[typeId] ?: return null - val result = when (type.typeCase) { - ProtoTypeDefinition.TypeCase.POINTER -> - modelNamedTypeForFieldAccess(index, underlyingToNamed, type.pointer.elemTypeId, active) - ProtoTypeDefinition.TypeCase.NAMED_REF -> type.namedRef.namedTypeId - ProtoTypeDefinition.TypeCase.STRUCT_TYPE -> underlyingToNamed[typeId] - else -> null - } - active -= typeId - return result -} - -private fun fieldDeclarationToStructField(field: ProtoFieldDecl): ProtoStructField { - return ProtoStructField.newBuilder() - .setName(field.name) - .setTypeId(field.typeId) - .setIndex(field.index) - .setEmbedded(field.embedded) - .setExported(field.exported) - .setTag(field.tag) - .build() -} - -private fun assignIds( - source: Map, - existing: MutableMap, - next: () -> Int, -): Map { - return buildMap { - source.toSortedMap().forEach { (oldId, key) -> - put(oldId, existing[key] ?: next().also { existing[key] = it }) - } - } -} - -private enum class MapName { - NONE, - TYPE, - PACKAGE, - FUNCTION, - NAMED, - GLOBAL, - CONST, -} - -private fun remapMessageIds(message: Message, remap: IdRemap): Message { - val builder = message.toBuilder() - message.allFields.forEach { (field, value) -> - if (field.isRepeated) { - val values = value as List<*> - if (field.javaType == FieldDescriptor.JavaType.MESSAGE) { - builder.clearField(field) - values.forEach { child -> - builder.addRepeatedField(field, remapMessageIds(child as Message, remap)) - } - } else { - val mapName = remapClass(message, field) - if (mapName != MapName.NONE) { - values.forEachIndexed { index, oldId -> - builder.setRepeatedField( - field, - index, - remapValue(oldId as Int, remap.values(mapName), message, field), - ) - } - } - } - } else if (field.javaType == FieldDescriptor.JavaType.MESSAGE) { - builder.setField(field, remapMessageIds(value as Message, remap)) - } else { - val mapName = remapClass(message, field) - if (mapName != MapName.NONE) { - builder.setField( - field, - remapValue(value as Int, remap.values(mapName), message, field), - ) - } - } - } - return builder.build() -} - -private fun remapMessageIdsOfClass( - message: Message, - mapName: MapName, - remap: (Int) -> Int, -): Message { - val builder = message.toBuilder() - message.allFields.forEach { (field, value) -> - if (field.isRepeated) { - val values = value as List<*> - if (field.javaType == FieldDescriptor.JavaType.MESSAGE) { - builder.clearField(field) - values.forEach { child -> - builder.addRepeatedField( - field, - remapMessageIdsOfClass(child as Message, mapName, remap), - ) - } - } else if (remapClass(message, field) == mapName) { - values.forEachIndexed { index, oldId -> - builder.setRepeatedField(field, index, remap(oldId as Int)) - } - } - } else if (field.javaType == FieldDescriptor.JavaType.MESSAGE) { - builder.setField(field, remapMessageIdsOfClass(value as Message, mapName, remap)) - } else if (remapClass(message, field) == mapName) { - builder.setField(field, remap(value as Int)) - } - } - return builder.build() -} - -private fun remapValue( - oldId: Int, - values: Map, - message: Message, - field: FieldDescriptor, -): Int { - if (oldId == 0) { - return 0 - } - return values[oldId] ?: throw IllegalArgumentException( - "remapping ${message.descriptorForType.name}.${field.name}: " + - "wire id $oldId has no model remapping", - ) -} - -private fun validateReferences(program: ProtoProgram, source: String) { - val validIds = mapOf( - MapName.TYPE to program.typesList.mapTo(mutableSetOf()) { it.id }, - MapName.PACKAGE to program.packagesList.mapTo(mutableSetOf()) { it.id }, - MapName.FUNCTION to program.packagesList - .flatMapTo(mutableSetOf()) { pkg -> pkg.functionsList.map { it.id } }, - MapName.NAMED to program.packagesList - .flatMapTo(mutableSetOf()) { pkg -> pkg.namedTypesList.map { it.id } }, - MapName.GLOBAL to program.packagesList - .flatMapTo(mutableSetOf()) { pkg -> pkg.globalsList.map { it.id } }, - MapName.CONST to program.packagesList - .flatMapTo(mutableSetOf()) { pkg -> pkg.constantsList.map { it.id } }, - ) - - fun validate(message: Message, path: String) { - message.allFields.forEach { (field, value) -> - if (field.javaType == FieldDescriptor.JavaType.MESSAGE) { - if (field.isRepeated) { - (value as List<*>).forEachIndexed { index, child -> - validate(child as Message, "$path.${field.name}[$index]") - } - } else { - validate(value as Message, "$path.${field.name}") - } - return@forEach - } - val mapName = remapClass(message, field) - if (mapName == MapName.NONE) { - return@forEach - } - val idsInProgram = validIds.getValue(mapName) - val ids = if (field.isRepeated) value as List<*> else listOf(value) - ids.forEach { rawId -> - val id = rawId as Int - require(id == 0 || id in idsInProgram) { - "merging Go model $source: $path.${field.name} references missing " + - "${mapName.name.lowercase()} $id" - } - } - } - } - - validate(program, "ProtoProgram") -} - -private fun remapClass(message: Message, field: FieldDescriptor): MapName { - val name = field.name - if (name == "id") { - return when (message.descriptorForType.name) { - "ProtoTypeDefinition" -> MapName.TYPE - "ProtoPackage" -> MapName.PACKAGE - "ProtoNamedType" -> MapName.NAMED - "ProtoFunction" -> MapName.FUNCTION - "ProtoGlobal" -> MapName.GLOBAL - "ProtoConst" -> MapName.CONST - else -> MapName.NONE - } - } - return when { - name == "named_type_id" || name == "embedded_interface_ids" -> MapName.NAMED - name == "package_id" || name == "import_ids" -> MapName.PACKAGE - name == "fn_id" || name == "method_ids" || name == "pointer_method_ids" || - "function_id" in name -> MapName.FUNCTION - name == "global_id" -> MapName.GLOBAL - name == "type_arg_ids" || name.endsWith("type_id") || name.endsWith("type_ids") -> MapName.TYPE - else -> MapName.NONE - } -} - -private fun appendUniqueIds(base: List, added: List): List { - val result = base.toMutableList() - val seen = base.toMutableSet() - added.forEach { value -> - if (value != 0 && seen.add(value)) { - result += value - } - } - return result -} - -private fun mergeById(base: List, added: List, id: (T) -> Int): List { - val result = base.toMutableList() - val seen = base.mapTo(mutableSetOf(), id) - added.forEach { value -> - if (seen.add(id(value))) { - result += value - } - } - return result -} - -private fun Map.reverse(): MutableMap { - return entries.associateTo(mutableMapOf()) { (key, value) -> value to key } -} - -private const val GO_MODEL_PREFIX = "opentaint/" -private const val UNIVERSE_PACKAGE_PATH = "" -private const val MODEL_SUPPORT_KIND = "opentaint model support" -private const val MODEL_KIND = "opentaint model" diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/ModelPathNormalizer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/ModelPathNormalizer.kt new file mode 100644 index 0000000000..03621bfc84 --- /dev/null +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/ModelPathNormalizer.kt @@ -0,0 +1,45 @@ +package org.opentaint.ir.go.client + +import org.opentaint.ir.go.proto.ProtoProgram + +internal data class ModelPathNormalizer( + val replacements: Map, + val targets: Set, + private val orderedModelPaths: List, +) { + fun path(value: String): String = replacements[value] ?: value + + fun symbol(value: String): String = orderedModelPaths.fold(value) { result, modelPath -> + result.replace(modelPath, replacements.getValue(modelPath)) + } + + companion object { + fun create(program: ProtoProgram): ModelPathNormalizer { + val replacements = program.packagesList + .filterNot { it.isDependency || it.isStdlib || it.importPath == UNIVERSE_PACKAGE_PATH } + .associate { pkg -> + require(pkg.importPath.startsWith(GO_MODEL_PREFIX)) { + "Go model package \"${pkg.importPath}\" is not model-addressed, " + + "its import path must be ${GO_MODEL_PREFIX}" + } + val target = pkg.importPath.removePrefix(GO_MODEL_PREFIX) + require(target.isNotEmpty()) { + "Go model package \"${pkg.importPath}\" has an empty target package" + } + pkg.importPath to target + } + require(replacements.isNotEmpty()) { + "Go model contains no model-addressed packages, expected at least one package " + + "under $GO_MODEL_PREFIX" + } + return ModelPathNormalizer( + replacements, + replacements.values.toSet(), + replacements.keys.sortedByDescending(String::length), + ) + } + } +} + +private const val GO_MODEL_PREFIX = "opentaint/" +private const val UNIVERSE_PACKAGE_PATH = "" diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt index 22cb1bf862..9196c26163 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt @@ -10,11 +10,12 @@ import org.opentaint.ir.go.api.GoIRPosition import org.opentaint.ir.go.api.GoIRTypeParamDecl import org.opentaint.ir.go.api.GoIrFunctionReference import org.opentaint.ir.go.type.GoIRFuncType +import org.opentaint.ir.go.type.GoIRType class GoIRFunctionImpl( override val name: String, override val fullName: String, - override val pkg: GoIRPackage?, + pkg: GoIRPackage?, override val signature: GoIRFuncType, override val params: List, override val freeVars: List, @@ -22,13 +23,32 @@ class GoIRFunctionImpl( override val isMethod: Boolean, override val isPointerReceiver: Boolean, override val isExported: Boolean, - override val isSynthetic: Boolean, - override val syntheticKind: String?, + isSynthetic: Boolean, + syntheticKind: String?, private val declaredHasBody: Boolean = false, override val parent: GoIrFunctionReference?, override val anonymousFunctions: List, override val typeParams: List = emptyList(), + override val originFullName: String? = null, + override val typeArgs: List = emptyList(), ) : GoIRFunction { + private var owner = pkg + private var synthetic = isSynthetic + private var kind = syntheticKind + + override val isSynthetic: Boolean get() = synthetic + override val syntheticKind: String? get() = kind + override val pkg: GoIRPackage? get() = owner + + internal fun markAsModel(kind: String) { + synthetic = true + this.kind = kind + } + + internal fun rebindPackage(pkg: GoIRPackage) { + owner = pkg + } + private var _body: GoIRBody? = null override val body: GoIRBody? diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRGlobalImpl.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRGlobalImpl.kt index 2bd886354f..7b31f8c6a1 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRGlobalImpl.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRGlobalImpl.kt @@ -8,10 +8,17 @@ class GoIRGlobalImpl( override val name: String, override val fullName: String, override val type: GoIRType, - override val pkg: GoIRPackage, + pkg: GoIRPackage, override val isExported: Boolean, override val position: GoIRPosition?, ) : GoIRGlobal { + private var owner = pkg + override val pkg: GoIRPackage get() = owner + + internal fun rebindPackage(pkg: GoIRPackage) { + owner = pkg + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is GoIRGlobal) return false @@ -26,10 +33,17 @@ class GoIRConstImpl( override val fullName: String, override val type: GoIRType, override val value: GoIRConstantValue, - override val pkg: GoIRPackage, + pkg: GoIRPackage, override val isExported: Boolean, override val position: GoIRPosition?, ) : GoIRConst { + private var owner = pkg + override val pkg: GoIRPackage get() = owner + + internal fun rebindPackage(pkg: GoIRPackage) { + owner = pkg + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is GoIRConst) return false diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRNamedTypeImpl.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRNamedTypeImpl.kt index 97fc11861b..85dcb93626 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRNamedTypeImpl.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRNamedTypeImpl.kt @@ -13,12 +13,19 @@ import org.opentaint.ir.go.type.GoIRType class GoIRNamedTypeImpl( override val name: String, override val fullName: String, - override val pkg: GoIRPackage, + pkg: GoIRPackage, override val underlying: GoIRType, override val kind: GoIRNamedTypeKind, override val position: GoIRPosition?, override val typeParams: List = emptyList(), ) : GoIRNamedType { + private var owner = pkg + override val pkg: GoIRPackage get() = owner + + internal fun rebindPackage(pkg: GoIRPackage) { + owner = pkg + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is GoIRNamedType) return false diff --git a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt index 96ffc9acf7..e09b5da46b 100644 --- a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt +++ b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt @@ -16,6 +16,7 @@ import org.opentaint.ir.go.inst.GoIRGlobalStore import org.opentaint.ir.go.inst.GoIRReturn import org.opentaint.ir.go.test.GoIRTestBuilder import org.opentaint.ir.go.test.GoIRTestExtension +import org.opentaint.ir.go.type.GoIRStructType import org.opentaint.ir.go.value.GoIRConstValue import org.opentaint.ir.go.value.GoIRConstantValue import org.opentaint.ir.go.value.GoIRParameterValue @@ -53,6 +54,39 @@ class ModelTests { assertThat(returned.type).isEqualTo(function.params.single().type) } + @Test + fun `lazy model lookup keeps the original Go IR visible`(builder: GoIRTestBuilder) { + val project = project( + "example.com/original", + "package original\n" + + "func Identity(value string) string { return \"original\" }\n", + ) + val model = model( + "example.com/original", + "package original\n" + + "func helper(value string) string { return value }\n" + + "func Identity(value string) string { return helper(value) }\n", + ) + + val effectiveProgram = builder.buildFromDir(project, modelConfig(model)) + val effectivePackage = effectiveProgram.findPackage("example.com/original")!! + val effectiveIdentity = effectivePackage.functions.single { it.name == "Identity" } + val originalProgram = effectiveProgram.originalProgram + val originalPackage = originalProgram.findPackage("example.com/original")!! + val originalIdentity = originalPackage.functions.single { it.name == "Identity" } + + assertThat(originalProgram).isNotSameAs(effectiveProgram) + assertThat(effectiveIdentity.pkg).isSameAs(effectivePackage) + assertThat(effectiveIdentity.syntheticKind).isEqualTo("opentaint model") + assertThat(effectiveIdentity.findInstructions()) + .anyMatch { it.call.target?.displayName == "example.com/original.helper" } + assertThat(originalIdentity.isSynthetic).isFalse() + assertThat(originalIdentity.syntheticKind).isNull() + assertThat(originalIdentity.body!!.instructions) + .noneMatch { instruction -> instruction is GoIRCall } + assertThat(originalPackage.functions.map { it.name }).doesNotContain("helper") + } + @Test fun `partial model adds fields and methods after client deserialization`(builder: GoIRTestBuilder) { val project = Files.createTempDirectory("goir-model-fields-project") @@ -91,6 +125,8 @@ class ModelTests { val box = targetPackage.findNamedType("Box")!! assertThat(box.fields.map { it.name to it.index }) .containsExactly("Original" to 0, "Shadow" to 1) + assertThat((box.underlying as GoIRStructType).fields.map { it.name }) + .containsExactly("Original", "Shadow") assertThat(box.pointerMethods.map { it.name }).contains("Put", "Keep", "Helper") val put = box.pointerMethods.single { it.name == "Put" } @@ -475,6 +511,55 @@ class ModelTests { assertThat((returned as GoIRParameterValue).paramIndex).isZero() } + @Test + fun `model call into modeled dependency resolves through effective lookup`(builder: GoIRTestBuilder) { + val project = project( + "example.com/dependencycall", + "package dependencycall\n" + + "func Helper(value string) string { return \"original\" }\n" + + "func Identity(value string) string { return \"original\" }\n", + ) + val model = model( + "example.com/dependencycall", + "package dependencycall\n" + + "import target \"example.com/dependencycall\"\n" + + "func Identity(value string) string { return target.Helper(value) }\n", + ) + + val program = builder.buildFromDir(project, modelConfig(model)) + val targetPackage = program.findPackage("example.com/dependencycall")!! + val helper = targetPackage.functions.single { it.name == "Helper" && it.parent == null } + val identity = targetPackage.functions.single { it.name == "Identity" && it.parent == null } + val callTarget = identity.findInstructions().single().call.target + + assertThat(callTarget).isInstanceOf(GoIRCallTarget.Function::class.java) + assertThat((callTarget as GoIRCallTarget.Function).function).isSameAs(helper) + } + + @Test + fun `model-only dependency is visible only through effective lookup`(builder: GoIRTestBuilder) { + val project = project( + "example.com/modeldependency", + "package modeldependency\n" + + "func Identity(value string) string { return \"original\" }\n", + ) + val model = model( + "example.com/modeldependency", + "package modeldependency\n" + + "import \"net/url\"\n" + + "func Identity(value string) string { return url.QueryEscape(value) }\n", + ) + + val program = builder.buildFromDir(project, modelConfig(model)) + val identity = program.findPackage("example.com/modeldependency")!! + .functions.single { it.name == "Identity" && it.parent == null } + val call = identity.findInstructions().single() + + assertThat(program.originalProgram.findPackage("net/url")).isNull() + assertThat(program.findPackage("net/url")).isNotNull() + assertThat(call.call.target?.displayName).isEqualTo("net/url.QueryEscape") + } + @Test fun `imported target declarations do not damage generic target types`(builder: GoIRTestBuilder) { val project = project( From 95ea9d28ac78cfe805f377ffd0b508f60bcb4e76 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Wed, 2 Sep 2026 18:56:34 +0200 Subject: [PATCH 07/12] fix(go-models): resolve calls through lazy overlays --- .../kotlin/org/opentaint/dataflow/go/GoCallResolver.kt | 8 ++++++-- .../main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt | 2 ++ .../kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt | 8 ++++++++ .../org/opentaint/ir/go/test/features/ModelTests.kt | 3 +++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoCallResolver.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoCallResolver.kt index 0f2ab01a80..174b79f805 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoCallResolver.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoCallResolver.kt @@ -46,13 +46,13 @@ class GoCallResolver( private fun resolveDirect(call: GoIRCallInfo): List? { val target = call.target as? GoIRCallTarget.Function ?: return null - return listOf(target.function) + return listOf(resolveEffectiveFunction(target.function)) } private fun resolveDynamic(call: GoIRCallInfo, location: GoIRInst): List? { val target = call.target ?: return null when (target) { - is GoIRCallTarget.Function -> return listOf(target.function) + is GoIRCallTarget.Function -> return listOf(resolveEffectiveFunction(target.function)) is GoIRCallTarget.Dynamic -> { val method = location.location.functionBody.function val funcValue = target.value as? GoIRRegister ?: return null @@ -66,6 +66,10 @@ class GoCallResolver( } } + private fun resolveEffectiveFunction(function: GoIRFunction): GoIRFunction { + return cp.effectiveFunction(function) + } + private fun resolveInvoke(call: GoIRCallInfo): List? { val methodName = call.methodName ?: return null val receiverType = call.receiver?.type ?: return null diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt index 8b98251d78..43ea0a63c9 100644 --- a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt @@ -13,6 +13,8 @@ interface GoIRProgram { val anonymousInterfaces: Map fun findPackage(importPath: String): GoIRPackage? + /** Returns the effective function for a reference from [originalProgram]. */ + fun effectiveFunction(function: GoIRFunction): GoIRFunction = function fun allFunctions(): List fun allNamedTypes(): List fun mainPackage(): GoIRPackage? diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt index 9603528ba1..7b9864e319 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt @@ -102,6 +102,14 @@ internal class GoIRModelOverlayProgram( } } + override fun effectiveFunction(function: GoIRFunction): GoIRFunction { + val importPath = function.pkg?.importPath ?: return function + return findPackage(importPath) + ?.functions + ?.firstOrNull { it.fullName == function.fullName } + ?: function + } + override fun allFunctions(): List = packages.values.flatMap { it.functions } override fun allNamedTypes(): List = packages.values.flatMap { it.namedTypes } diff --git a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt index e09b5da46b..be51fa2532 100644 --- a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt +++ b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt @@ -48,7 +48,10 @@ class ModelTests { assertThat(program.findPackage("opentaint/strings")).isNull() val function = program.findPackage("strings")!!.functions.single { it.name == "ToUpper" } + val caller = program.findPackage("example.com/app")!!.functions.single { it.name == "Upper" } + val originalTarget = caller.findInstructions().single().call.target as GoIRCallTarget.Function assertThat(function.bodyAvailable).isTrue() + assertThat(program.effectiveFunction(originalTarget.function)).isSameAs(function) val returned = (function.body!!.blocks.single().terminator as GoIRReturn).results.single() as GoIRParameterValue assertThat(returned.paramIndex).isZero() assertThat(returned.type).isEqualTo(function.params.single().type) From 42a727bebbd18a78ff832c536c4b33180bfc02c7 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Thu, 22 Oct 2026 13:45:58 +0200 Subject: [PATCH 08/12] refactor(go-models): apply typed models through IR transformers --- .../opentaint/dataflow/go/GoCallResolver.kt | 8 +- .../opentaint/ir/go/api/GoIRModelFunction.kt | 9 + .../org/opentaint/ir/go/api/GoIRProgram.kt | 2 - .../opentaint/ir/go/api/GoIRTransformer.kt | 8 + .../ir/go/client/GoIRBodyTransformer.kt | 198 +++++++++++++++++ .../org/opentaint/ir/go/client/GoIRClient.kt | 2 +- .../ir/go/client/GoIRDeserializer.kt | 31 +-- .../opentaint/ir/go/client/GoIRModelLoader.kt | 23 ++ .../ir/go/client/GoIRModelOverlay.kt | 209 ++++-------------- .../ir/go/client/GoIRModelSpecializer.kt | 189 +--------------- .../ir/go/client/GoIRTransformerChain.kt | 205 +++++++++++++++++ .../opentaint/ir/go/impl/GoIRFunctionImpl.kt | 14 +- .../ir/go/test/features/ModelTests.kt | 100 ++++++++- .../ir/approximation/Approximations.kt | 50 +---- .../annotation/ApproximationAnnotations.kt | 11 - .../target/KotlinClassName.java | 4 +- .../go/sast/dataflow/GoUnitResolver.kt | 6 +- .../go/sast/dataflow/GoUnitResolverTest.kt | 15 ++ 18 files changed, 622 insertions(+), 462 deletions(-) create mode 100644 core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRModelFunction.kt create mode 100644 core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRTransformer.kt create mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyTransformer.kt create mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt create mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoCallResolver.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoCallResolver.kt index 174b79f805..0f2ab01a80 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoCallResolver.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoCallResolver.kt @@ -46,13 +46,13 @@ class GoCallResolver( private fun resolveDirect(call: GoIRCallInfo): List? { val target = call.target as? GoIRCallTarget.Function ?: return null - return listOf(resolveEffectiveFunction(target.function)) + return listOf(target.function) } private fun resolveDynamic(call: GoIRCallInfo, location: GoIRInst): List? { val target = call.target ?: return null when (target) { - is GoIRCallTarget.Function -> return listOf(resolveEffectiveFunction(target.function)) + is GoIRCallTarget.Function -> return listOf(target.function) is GoIRCallTarget.Dynamic -> { val method = location.location.functionBody.function val funcValue = target.value as? GoIRRegister ?: return null @@ -66,10 +66,6 @@ class GoCallResolver( } } - private fun resolveEffectiveFunction(function: GoIRFunction): GoIRFunction { - return cp.effectiveFunction(function) - } - private fun resolveInvoke(call: GoIRCallInfo): List? { val methodName = call.methodName ?: return null val receiverType = call.receiver?.type ?: return null diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRModelFunction.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRModelFunction.kt new file mode 100644 index 0000000000..b7ffb06c56 --- /dev/null +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRModelFunction.kt @@ -0,0 +1,9 @@ +package org.opentaint.ir.go.api + +/** A function supplied by a model, including helpers with no original declaration. */ +interface GoIRModelFunction : GoIRFunction + +/** A model implementation of an existing program function. */ +interface GoIRApproximatedFunction : GoIRModelFunction { + val original: GoIRFunction +} diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt index 43ea0a63c9..8b98251d78 100644 --- a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt @@ -13,8 +13,6 @@ interface GoIRProgram { val anonymousInterfaces: Map fun findPackage(importPath: String): GoIRPackage? - /** Returns the effective function for a reference from [originalProgram]. */ - fun effectiveFunction(function: GoIRFunction): GoIRFunction = function fun allFunctions(): List fun allNamedTypes(): List fun mainPackage(): GoIRPackage? diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRTransformer.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRTransformer.kt new file mode 100644 index 0000000000..0715842c1d --- /dev/null +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRTransformer.kt @@ -0,0 +1,8 @@ +package org.opentaint.ir.go.api + +/** Lookup transformations, applied in list order before the resulting IR is exposed. */ +interface GoIRTransformer { + fun transformPackage(pkg: GoIRPackage): GoIRPackage = pkg + fun transformFunction(function: GoIRFunction): GoIRFunction = function + val additionalPackages: List get() = emptyList() +} diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyTransformer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyTransformer.kt new file mode 100644 index 0000000000..dd55a9f958 --- /dev/null +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyTransformer.kt @@ -0,0 +1,198 @@ +package org.opentaint.ir.go.client + +import java.util.IdentityHashMap +import org.opentaint.ir.go.api.GoIRBody +import org.opentaint.ir.go.api.GoIRFunction +import org.opentaint.ir.go.api.GoIRGlobal +import org.opentaint.ir.go.cfg.GoIRCallInfo +import org.opentaint.ir.go.cfg.GoIRCallTarget +import org.opentaint.ir.go.cfg.GoIRSelectState +import org.opentaint.ir.go.expr.* +import org.opentaint.ir.go.impl.GoIRBasicBlockImpl +import org.opentaint.ir.go.impl.GoIRBodyImpl +import org.opentaint.ir.go.inst.* +import org.opentaint.ir.go.type.* +import org.opentaint.ir.go.value.* + +internal fun transformGoIRBody( + source: GoIRBody, + target: GoIRFunction, + type: (GoIRType) -> GoIRType, + functions: (GoIRFunction) -> GoIRFunction, + globals: (GoIRGlobal) -> GoIRGlobal = { it }, +): GoIRBody { + val blocks = source.blocks.map { GoIRBasicBlockImpl(it.index, it.label) } + val recover = source.recoverBlock?.let { blocks[it.index] } + val body = GoIRBodyImpl(target, blocks, recover) + val registers = IdentityHashMap() + source.instructions.filterIsInstance().forEach { definition -> + registers[definition.register] = GoIRRegister( + type(definition.register.type), + definition.register.index, + definition.register.name, + ) + } + val copier = BodyCopier(body, type, functions, globals, registers) + source.blocks.forEach { oldBlock -> + val block = blocks[oldBlock.index] + block.setInstructions(oldBlock.instructions.map(copier::instruction)) + block.predIndices = oldBlock.predecessors.map { it.index } + block.succIndices = oldBlock.successors.map { it.index } + block.idomIndex = oldBlock.idom?.index ?: -1 + block.domineeIndices = oldBlock.dominatedBlocks.map { it.index } + } + blocks.forEach { block -> + block.resolvePredecessors(blocks) + block.resolveSuccessors(blocks) + block.resolveIdom(blocks) + block.resolveDominees(blocks) + } + return body +} + +private class BodyCopier( + private val body: GoIRBody, + private val type: (GoIRType) -> GoIRType, + private val functions: (GoIRFunction) -> GoIRFunction, + private val globals: (GoIRGlobal) -> GoIRGlobal, + private val registers: IdentityHashMap, +) { + private fun value(value: GoIRValue): GoIRValue = when (value) { + is GoIRRegister -> registers[value] ?: error("No copied register ${value.index}") + is GoIRParameterValue -> GoIRParameterValue(type(value.type), value.name, value.paramIndex) + is GoIRConstValue -> GoIRConstValue(type(value.type), value.name, value.value) + else -> error("Unsupported Go IR value ${value::class.simpleName}") + } + + private fun location(source: GoInstLocation) = + GoInstLocation(body, source.index, source.blockIndex, source.position) + + private fun call(source: GoIRCallInfo): GoIRCallInfo = GoIRCallInfo( + mode = source.mode, + target = when (val target = source.target) { + is GoIRCallTarget.Function -> GoIRCallTarget.Function( + functions(target.function), + ) + is GoIRCallTarget.Builtin -> GoIRCallTarget.Builtin(target.name, type(target.type)) + is GoIRCallTarget.Dynamic -> GoIRCallTarget.Dynamic(value(target.value)) + null -> null + }, + receiver = source.receiver?.let(::value), + methodName = source.methodName, + args = source.args.map(::value), + resultType = type(source.resultType), + ) + + private fun expression(source: GoIRExpr): GoIRExpr = when (source) { + is GoIRAllocExpr -> source.copy(type = type(source.type), allocType = type(source.allocType)) + is GoIRBinOpExpr -> source.copy(type = type(source.type), x = value(source.x), y = value(source.y)) + is GoIRUnOpExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRChangeTypeExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRConvertExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRMultiConvertExpr -> source.copy( + type = type(source.type), + x = value(source.x), + fromType = type(source.fromType), + toType = type(source.toType), + ) + is GoIRChangeInterfaceExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRSliceToArrayPointerExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRMakeInterfaceExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRTypeAssertExpr -> source.copy( + type = type(source.type), + x = value(source.x), + assertedType = type(source.assertedType), + ) + is GoIRMakeClosureExpr -> source.copy( + type = type(source.type), + fn = functions(source.fn), + bindings = source.bindings.map(::value), + ) + is GoIRMakeMapExpr -> source.copy(type = type(source.type), reserve = source.reserve?.let(::value)) + is GoIRMakeChanExpr -> source.copy(type = type(source.type), size = value(source.size)) + is GoIRMakeSliceExpr -> source.copy( + type = type(source.type), + len = value(source.len), + cap = value(source.cap), + ) + is GoIRFieldAddrExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRFieldExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRIndexAddrExpr -> source.copy( + type = type(source.type), x = value(source.x), indexValue = value(source.indexValue), + ) + is GoIRIndexExpr -> source.copy( + type = type(source.type), x = value(source.x), indexValue = value(source.indexValue), + ) + is GoIRSliceExpr -> source.copy( + type = type(source.type), + x = value(source.x), + low = source.low?.let(::value), + high = source.high?.let(::value), + max = source.max?.let(::value), + ) + is GoIRLookupExpr -> source.copy( + type = type(source.type), x = value(source.x), indexValue = value(source.indexValue), + ) + is GoIRRangeExpr -> source.copy(type = type(source.type), x = value(source.x)) + is GoIRNextExpr -> source.copy(type = type(source.type), iter = value(source.iter)) + is GoIRSelectExpr -> source.copy( + type = type(source.type), + states = source.states.map { + GoIRSelectState(it.direction, value(it.chan), it.send?.let(::value), it.position) + }, + ) + is GoIRExtractExpr -> source.copy(type = type(source.type), tuple = value(source.tuple)) + is GoIRGlobalValueExpr -> source.copy(type = type(source.type), global = globals(source.global)) + is GoIRFunctionValueExpr -> source.copy( + type = type(source.type), function = functions(source.function), + ) + is GoIRBuiltinValueExpr -> source.copy(type = type(source.type)) + is GoIRFreeVarValueExpr -> source.copy(type = type(source.type)) + } + + fun instruction(source: GoIRInst): GoIRInst { + val location = location(source.location) + return when (source) { + is GoIRAssignInst -> GoIRAssignInst(location, registers.getValue(source.register), expression(source.expr)) + is GoIRPhi -> GoIRPhi( + location, + registers.getValue(source.register), + source.edges.mapValues { value(it.value) }, + source.comment, + ) + is GoIRCall -> GoIRCall(location, registers.getValue(source.register), call(source.call)) + is GoIRJump -> source.copy(location = location) + is GoIRIf -> source.copy(location = location, cond = value(source.cond)) + is GoIRReturn -> source.copy(location = location, results = source.results.map(::value)) + is GoIRPanic -> source.copy(location = location, x = value(source.x)) + is GoIRFieldStore -> source.copy( + location = location, + addr = value(source.addr), + base = value(source.base), + value = value(source.value), + ) + is GoIRIndexStore -> source.copy( + location = location, + addr = value(source.addr), + base = value(source.base), + index = value(source.index), + value = value(source.value), + ) + is GoIRStore -> source.copy( + location = location, addr = value(source.addr), value = value(source.value), + ) + is GoIRGlobalStore -> source.copy(location = location, global = globals(source.global), value = value(source.value)) + is GoIRMapUpdate -> source.copy( + location = location, + map = value(source.map), + key = value(source.key), + value = value(source.value), + ) + is GoIRSend -> source.copy(location = location, chan = value(source.chan), x = value(source.x)) + is GoIRGo -> source.copy(location = location, call = call(source.call)) + is GoIRDefer -> source.copy(location = location, call = call(source.call)) + is GoIRRunDefers -> source.copy(location = location) + is GoIRDebugRef -> source.copy(location = location, x = value(source.x)) + } + } +} diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRClient.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRClient.kt index 997d90b256..f6258c9c60 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRClient.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRClient.kt @@ -49,7 +49,7 @@ class GoIRClient : AutoCloseable { val deserializeStart = System.nanoTime() val program = try { val responses = stub.buildProgram(request) - deserializer.deserialize(responses) + GoIRModelLoader.apply(deserializer.deserialize(responses)) } catch (e: StatusRuntimeException) { // gRPC only says e.g. "UNAVAILABLE: Network closed" when the server // process dies mid-stream. Attach the server's exit state and stderr diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt index a3e203c2c2..d6211dc843 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt @@ -136,7 +136,7 @@ class GoIRDeserializer { var serverBuildTimeMs: Long = 0L private set - fun deserialize(responses: Iterator): GoIRProgram { + internal fun deserialize(responses: Iterator): DeserializedGoIRBuild { var program: ProtoProgram? = null val models = mutableListOf() for (r in responses) { @@ -157,28 +157,15 @@ class GoIRDeserializer { } check(program != null) { "Unexpected IR build issue" } - val original = deserializeProgram(program) - if (models.isEmpty()) { - return original - } - - val deserializedModels = models.map { model -> - val normalizer = try { - ModelPathNormalizer.create(model.program) - } catch (error: IllegalArgumentException) { - throw IllegalArgumentException("invalid Go model ${model.source}: ${error.message}", error) - } - val ownedPaths = normalizer.replacements.keys - val modelProgram = GoIRDeserializer().deserializeProgram( - model.program, - DeserializationConfig(normalizer, ownedPaths), - ) - GoIRModelDefinition(model.source, modelProgram, normalizer.targets) - } - return GoIRModelOverlayProgram(original, deserializedModels) + return DeserializedGoIRBuild(deserializeProgram(program), models) } - private data class DeserializationConfig( + internal data class DeserializedGoIRBuild( + val program: GoIRProgram, + val models: List, + ) + + internal data class DeserializationConfig( val normalizer: ModelPathNormalizer? = null, val ownedPackagePaths: Set = emptySet(), ) { @@ -187,7 +174,7 @@ class GoIRDeserializer { fun owns(pkg: ProtoPackage): Boolean = pkg.importPath in ownedPackagePaths } - private fun deserializeProgram( + internal fun deserializeProgram( serializedProgram: ProtoProgram, config: DeserializationConfig = DeserializationConfig(), ): GoIRProgram { diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt new file mode 100644 index 0000000000..934e52dfe3 --- /dev/null +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt @@ -0,0 +1,23 @@ +package org.opentaint.ir.go.client + +import org.opentaint.ir.go.api.GoIRProgram + +/** Model policy and transformer installation, separate from wire deserialization. */ +internal object GoIRModelLoader { + fun apply(build: GoIRDeserializer.DeserializedGoIRBuild): GoIRProgram { + if (build.models.isEmpty()) return build.program + val definitions = build.models.map { model -> + val normalizer = try { + ModelPathNormalizer.create(model.program) + } catch (error: IllegalArgumentException) { + throw IllegalArgumentException("invalid Go model ${model.source}: ${error.message}", error) + } + val program = GoIRDeserializer().deserializeProgram( + model.program, + GoIRDeserializer.DeserializationConfig(normalizer, normalizer.replacements.keys), + ) + GoIRModelDefinition(model.source, program, normalizer.targets) + } + return GoIRTransformerChain(build.program, listOf(GoIRModelTransformer(build.program, definitions))) + } +} diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt index 7b9864e319..96813353b9 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt @@ -1,32 +1,22 @@ package org.opentaint.ir.go.client +import org.opentaint.ir.go.api.GoIRApproximatedFunction import org.opentaint.ir.go.api.GoIRConst import org.opentaint.ir.go.api.GoIRField import org.opentaint.ir.go.api.GoIRFunction import org.opentaint.ir.go.api.GoIRGlobal import org.opentaint.ir.go.api.GoIRInterfaceMethod +import org.opentaint.ir.go.api.GoIRModelFunction import org.opentaint.ir.go.api.GoIRNamedType import org.opentaint.ir.go.api.GoIRPackage import org.opentaint.ir.go.api.GoIRProgram +import org.opentaint.ir.go.api.GoIRTransformer import org.opentaint.ir.go.api.GoIRTypeParamDecl -import org.opentaint.ir.go.impl.GoIRFunctionImpl -import org.opentaint.ir.go.impl.GoIRBasicBlockImpl -import org.opentaint.ir.go.impl.GoIRConstImpl -import org.opentaint.ir.go.impl.GoIRGlobalImpl -import org.opentaint.ir.go.impl.GoIRNamedTypeImpl import org.opentaint.ir.go.expr.GoIRFieldAddrExpr import org.opentaint.ir.go.expr.GoIRFieldExpr -import org.opentaint.ir.go.expr.GoIRFunctionValueExpr -import org.opentaint.ir.go.expr.GoIRGlobalValueExpr -import org.opentaint.ir.go.expr.GoIRMakeClosureExpr -import org.opentaint.ir.go.cfg.GoIRCallInfo -import org.opentaint.ir.go.cfg.GoIRCallTarget +import org.opentaint.ir.go.impl.GoIRBasicBlockImpl import org.opentaint.ir.go.inst.GoIRAssignInst -import org.opentaint.ir.go.inst.GoIRCall -import org.opentaint.ir.go.inst.GoIRDefer import org.opentaint.ir.go.inst.GoIRFieldStore -import org.opentaint.ir.go.inst.GoIRGlobalStore -import org.opentaint.ir.go.inst.GoIRGo import org.opentaint.ir.go.type.GoIRAnonymousInterfaceType import org.opentaint.ir.go.type.GoIRAnonymousInterfaceTypeRef import org.opentaint.ir.go.type.GoIRArrayType @@ -35,18 +25,16 @@ import org.opentaint.ir.go.type.GoIRChanType import org.opentaint.ir.go.type.GoIRFuncType import org.opentaint.ir.go.type.GoIRMapType import org.opentaint.ir.go.type.GoIRNamedInterfaceType -import org.opentaint.ir.go.type.GoIRNamedTypeRef import org.opentaint.ir.go.type.GoIRNamedTypeKind +import org.opentaint.ir.go.type.GoIRNamedTypeRef import org.opentaint.ir.go.type.GoIRPointerType import org.opentaint.ir.go.type.GoIRSliceType -import org.opentaint.ir.go.type.GoIRStructType import org.opentaint.ir.go.type.GoIRStructField +import org.opentaint.ir.go.type.GoIRStructType import org.opentaint.ir.go.type.GoIRTupleType import org.opentaint.ir.go.type.GoIRType import org.opentaint.ir.go.type.GoIRTypeParamRef import org.opentaint.ir.go.type.GoIRTypeParamType -import java.util.Collections -import java.util.IdentityHashMap internal data class GoIRModelDefinition( val source: String, @@ -60,7 +48,7 @@ internal class GoIRModelOverlayProgram( definitions: List, ) : GoIRProgram { private val definitionsByTarget = linkedMapOf() - private val modelDependencies = linkedMapOf() + val modelDependencies = linkedMapOf() private val effectivePackages = mutableMapOf() init { @@ -97,19 +85,13 @@ internal class GoIRModelOverlayProgram( override fun findPackage(importPath: String): GoIRPackage? { val original = originalProgram.findPackage(importPath) ?: return modelDependencies[importPath] val definition = definitionsByTarget[importPath] ?: return original - return effectivePackages.getOrPut(importPath) { - createPackage(original, definition.program.findPackage(importPath)!!) + return synchronized(effectivePackages) { + effectivePackages.getOrPut(importPath) { + createPackage(original, definition.program.findPackage(importPath)!!) + } } } - override fun effectiveFunction(function: GoIRFunction): GoIRFunction { - val importPath = function.pkg?.importPath ?: return function - return findPackage(importPath) - ?.functions - ?.firstOrNull { it.fullName == function.fullName } - ?: function - } - override fun allFunctions(): List = packages.values.flatMap { it.functions } override fun allNamedTypes(): List = packages.values.flatMap { it.namedTypes } @@ -157,154 +139,23 @@ internal class GoIRModelOverlayProgram( val replacement = modelByName[target.fullName] if (replacement != null && replacement.bodyAvailable && sameSignature(replacement, target)) { used += replacement - (replacement as? GoIRFunctionImpl)?.markAsModel(MODEL_KIND) - replacement + ApproximatedFunction(replacement, target) } else { val genericModel = target.originFullName?.let(modelByName::get) if (genericModel != null && genericModel.bodyAvailable) { val specialized = specializer.specialize(genericModel, target) specializedAnonymousFunctions += specialized.anonymousFunctions - specialized.root + ApproximatedFunction(specialized.root, target) } else { target } } }.toMutableList() - effectiveFunctions += specializedAnonymousFunctions - modelFunctions.filterTo(effectiveFunctions) { function -> - if (function in used) return@filterTo false - (function as? GoIRFunctionImpl)?.markAsModel(MODEL_SUPPORT_KIND) - true - } + effectiveFunctions += specializedAnonymousFunctions.map(::ModelFunction) + effectiveFunctions += modelFunctions.filter { it !in used }.map(::ModelFunction) - val effective = GoIRModelOverlayPackage(original, model, effectiveFunctions) - modelFunctions.forEach { (it as? GoIRFunctionImpl)?.rebindPackage(effective) } - specializedAnonymousFunctions.forEach { (it as? GoIRFunctionImpl)?.rebindPackage(effective) } - effectiveFunctions.filter { it.syntheticKind == MODEL_KIND } - .forEach { (it as? GoIRFunctionImpl)?.rebindPackage(effective) } - model.namedTypes.forEach { (it as? GoIRNamedTypeImpl)?.rebindPackage(effective) } - model.globals.forEach { (it as? GoIRGlobalImpl)?.rebindPackage(effective) } - model.constants.forEach { (it as? GoIRConstImpl)?.rebindPackage(effective) } - rebindModelPackage(model) remapModelFieldAccesses(original, model) - rebindModelEntities( - effective, - modelFunctions + specializedAnonymousFunctions + - effectiveFunctions.filter { it.syntheticKind == MODEL_KIND }, - ) - return effective - } - - private fun rebindModelEntities( - effective: GoIRPackage, - functions: List, - ) { - fun resolve(function: GoIRFunction): GoIRFunction { - if (function.pkg?.importPath != effective.importPath) return function - return effective.functions.firstOrNull { candidate -> - candidate.fullName == function.fullName && sameSignature(candidate, function) - } ?: function - } - fun resolve(global: GoIRGlobal): GoIRGlobal { - if (global.pkg.importPath != effective.importPath) return global - return effective.globals.firstOrNull { it.fullName == global.fullName } ?: global - } - fun call(source: GoIRCallInfo): GoIRCallInfo { - val target = source.target - if (target !is GoIRCallTarget.Function) return source - val rebound = resolve(target.function) - return if (rebound === target.function) source else source.copy( - target = GoIRCallTarget.Function(rebound), - ) - } - - val visitedFunctions = Collections.newSetFromMap(IdentityHashMap()) - functions.filter(visitedFunctions::add).forEach { function -> - val body = function.body ?: return@forEach - body.blocks.forEach { block -> - val mutableBlock = block as? GoIRBasicBlockImpl ?: return@forEach - mutableBlock.setInstructions(block.instructions.map { instruction -> - when (instruction) { - is GoIRCall -> instruction.copy(call = call(instruction.call)) - is GoIRGo -> instruction.copy(call = call(instruction.call)) - is GoIRDefer -> instruction.copy(call = call(instruction.call)) - is GoIRGlobalStore -> instruction.copy(global = resolve(instruction.global)) - is GoIRAssignInst -> when (val expression = instruction.expr) { - is GoIRFunctionValueExpr -> instruction.copy( - expr = expression.copy(function = resolve(expression.function)), - ) - is GoIRMakeClosureExpr -> instruction.copy( - expr = expression.copy(fn = resolve(expression.fn)), - ) - is GoIRGlobalValueExpr -> instruction.copy( - expr = expression.copy(global = resolve(expression.global)), - ) - else -> instruction - } - else -> instruction - } - }) - } - } - } - - private fun rebindModelPackage(model: GoIRPackage) { - val visited = Collections.newSetFromMap(IdentityHashMap()) - fun rebind(type: GoIRType) { - if (!visited.add(type)) return - when (type) { - is GoIRPointerType -> rebind(type.elem) - is GoIRArrayType -> rebind(type.elem) - is GoIRSliceType -> rebind(type.elem) - is GoIRMapType -> { - rebind(type.key) - rebind(type.value) - } - is GoIRChanType -> rebind(type.elem) - is GoIRStructType -> { - type.structTypeRef?.program = this - type.fields.forEach { rebind(it.type) } - } - is GoIRNamedTypeRef -> { - type.typeRef.program = this - type.typeArgs.forEach(::rebind) - } - is GoIRNamedInterfaceType -> { - type.interfaceTypeRef.program = this - type.methods.forEach { rebind(it.signature) } - type.embeds.forEach(::rebind) - } - is GoIRAnonymousInterfaceTypeRef -> {} - is GoIRAnonymousInterfaceType -> { - type.methods.forEach { rebind(it.signature) } - type.embeds.forEach(::rebind) - } - is GoIRFuncType -> { - type.params.forEach(::rebind) - type.results.forEach(::rebind) - type.recv?.let(::rebind) - } - is GoIRTupleType -> type.elements.forEach(::rebind) - is GoIRTypeParamType -> rebind(type.constraint) - else -> {} - } - } - model.namedTypes.forEach { type -> - rebind(type.underlying) - type.fields.forEach { rebind(it.type) } - type.typeParams.forEach { rebind(it.constraint) } - } - model.functions.forEach { function -> - rebind(function.signature) - function.params.forEach { rebind(it.type) } - function.freeVars.forEach { rebind(it.type) } - function.typeParams.forEach { rebind(it.constraint) } - function.typeArgs.forEach(::rebind) - function.parent?.program = this - function.anonymousFunctions.forEach { it.program = this } - } - model.globals.forEach { rebind(it.type) } - model.constants.forEach { rebind(it.type) } + return GoIRModelOverlayPackage(original, model, effectiveFunctions) } private fun remapModelFieldAccesses(original: GoIRPackage, model: GoIRPackage) { @@ -581,5 +432,29 @@ private class GoIRModelOverlayNamedType( } } -private const val MODEL_SUPPORT_KIND = "opentaint model support" -private const val MODEL_KIND = "opentaint model" +private open class ModelFunction(protected val implementation: GoIRFunction) : GoIRModelFunction, + GoIRFunction by implementation { + override val isSynthetic: Boolean get() = true +} + +private class ApproximatedFunction( + implementation: GoIRFunction, + override val original: GoIRFunction, +) : ModelFunction(implementation), GoIRApproximatedFunction + +/** Resolves models as a lookup transformer; deserialization never applies it. */ +internal class GoIRModelTransformer( + program: GoIRProgram, + definitions: List, +) : GoIRTransformer { + private val models = GoIRModelOverlayProgram(program, definitions) + + override val additionalPackages: List get() = models.modelDependencies.values.toList() + + override fun transformPackage(pkg: GoIRPackage): GoIRPackage = + models.findPackage(pkg.importPath) ?: pkg + + override fun transformFunction(function: GoIRFunction): GoIRFunction = + function.pkg?.importPath?.let(models::findPackage)?.functions + ?.firstOrNull { it.fullName == function.fullName } ?: function +} diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt index 675e73164f..b49d6f8133 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt @@ -62,7 +62,7 @@ internal class GoIRModelSpecializer( isPointerReceiver = target.isPointerReceiver, isExported = target.isExported, isSynthetic = true, - syntheticKind = MODEL_KIND_FOR_SPECIALIZER, + syntheticKind = modelRoot.syntheticKind, declaredHasBody = true, parent = target.parent, anonymousFunctions = rootRefs, @@ -90,7 +90,7 @@ internal class GoIRModelSpecializer( isPointerReceiver = source.isPointerReceiver, isExported = source.isExported, isSynthetic = true, - syntheticKind = MODEL_SUPPORT_KIND_FOR_SPECIALIZER, + syntheticKind = source.syntheticKind, declaredHasBody = true, parent = reference(parent), anonymousFunctions = refs, @@ -111,7 +111,7 @@ internal class GoIRModelSpecializer( (listOf(modelRoot) + sourceFunctions).forEach { source -> val copied = functionMap.getValue(source) - copied.setBody(copyBody(source.body!!, copied, specializeType, functionMap)) + copied.setBody(transformGoIRBody(source.body!!, copied, specializeType, { functionMap[it] ?: it })) } return SpecializedGoModelFunction(root, sourceFunctions.map(functionMap::getValue)) } @@ -128,40 +128,6 @@ internal class GoIRModelSpecializer( return result } - private fun copyBody( - source: GoIRBody, - target: GoIRFunctionImpl, - type: TypeSpecializer, - functions: IdentityHashMap, - ): GoIRBody { - val blocks = source.blocks.map { GoIRBasicBlockImpl(it.index, it.label) } - val recover = source.recoverBlock?.let { blocks[it.index] } - val body = GoIRBodyImpl(target, blocks, recover) - val registers = IdentityHashMap() - source.instructions.filterIsInstance().forEach { definition -> - registers[definition.register] = GoIRRegister( - type(definition.register.type), - definition.register.index, - definition.register.name, - ) - } - val copier = BodyCopier(body, type, functions, registers) - source.blocks.forEach { oldBlock -> - val block = blocks[oldBlock.index] - block.setInstructions(oldBlock.instructions.map(copier::instruction)) - block.predIndices = oldBlock.predecessors.map { it.index } - block.succIndices = oldBlock.successors.map { it.index } - block.idomIndex = oldBlock.idom?.index ?: -1 - block.domineeIndices = oldBlock.dominatedBlocks.map { it.index } - } - blocks.forEach { block -> - block.resolvePredecessors(blocks) - block.resolveSuccessors(blocks) - block.resolveIdom(blocks) - block.resolveDominees(blocks) - } - return body - } } private class TypeSpecializer( @@ -207,152 +173,3 @@ private class TypeSpecializer( return result } } - -private class BodyCopier( - private val body: GoIRBody, - private val type: TypeSpecializer, - private val functions: IdentityHashMap, - private val registers: IdentityHashMap, -) { - private fun value(value: GoIRValue): GoIRValue = when (value) { - is GoIRRegister -> registers[value] ?: error("No copied register ${value.index}") - is GoIRParameterValue -> GoIRParameterValue(type(value.type), value.name, value.paramIndex) - is GoIRConstValue -> GoIRConstValue(type(value.type), value.name, value.value) - else -> error("Unsupported Go IR value ${value::class.simpleName}") - } - - private fun location(source: GoInstLocation) = - GoInstLocation(body, source.index, source.blockIndex, source.position) - - private fun call(source: GoIRCallInfo): GoIRCallInfo = GoIRCallInfo( - mode = source.mode, - target = when (val target = source.target) { - is GoIRCallTarget.Function -> GoIRCallTarget.Function( - functions[target.function] ?: target.function, - ) - is GoIRCallTarget.Builtin -> GoIRCallTarget.Builtin(target.name, type(target.type)) - is GoIRCallTarget.Dynamic -> GoIRCallTarget.Dynamic(value(target.value)) - null -> null - }, - receiver = source.receiver?.let(::value), - methodName = source.methodName, - args = source.args.map(::value), - resultType = type(source.resultType), - ) - - private fun expression(source: GoIRExpr): GoIRExpr = when (source) { - is GoIRAllocExpr -> source.copy(type = type(source.type), allocType = type(source.allocType)) - is GoIRBinOpExpr -> source.copy(type = type(source.type), x = value(source.x), y = value(source.y)) - is GoIRUnOpExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRChangeTypeExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRConvertExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRMultiConvertExpr -> source.copy( - type = type(source.type), - x = value(source.x), - fromType = type(source.fromType), - toType = type(source.toType), - ) - is GoIRChangeInterfaceExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRSliceToArrayPointerExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRMakeInterfaceExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRTypeAssertExpr -> source.copy( - type = type(source.type), - x = value(source.x), - assertedType = type(source.assertedType), - ) - is GoIRMakeClosureExpr -> source.copy( - type = type(source.type), - fn = functions[source.fn] ?: source.fn, - bindings = source.bindings.map(::value), - ) - is GoIRMakeMapExpr -> source.copy(type = type(source.type), reserve = source.reserve?.let(::value)) - is GoIRMakeChanExpr -> source.copy(type = type(source.type), size = value(source.size)) - is GoIRMakeSliceExpr -> source.copy( - type = type(source.type), - len = value(source.len), - cap = value(source.cap), - ) - is GoIRFieldAddrExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRFieldExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRIndexAddrExpr -> source.copy( - type = type(source.type), x = value(source.x), indexValue = value(source.indexValue), - ) - is GoIRIndexExpr -> source.copy( - type = type(source.type), x = value(source.x), indexValue = value(source.indexValue), - ) - is GoIRSliceExpr -> source.copy( - type = type(source.type), - x = value(source.x), - low = source.low?.let(::value), - high = source.high?.let(::value), - max = source.max?.let(::value), - ) - is GoIRLookupExpr -> source.copy( - type = type(source.type), x = value(source.x), indexValue = value(source.indexValue), - ) - is GoIRRangeExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRNextExpr -> source.copy(type = type(source.type), iter = value(source.iter)) - is GoIRSelectExpr -> source.copy( - type = type(source.type), - states = source.states.map { - GoIRSelectState(it.direction, value(it.chan), it.send?.let(::value), it.position) - }, - ) - is GoIRExtractExpr -> source.copy(type = type(source.type), tuple = value(source.tuple)) - is GoIRGlobalValueExpr -> source.copy(type = type(source.type)) - is GoIRFunctionValueExpr -> source.copy( - type = type(source.type), function = functions[source.function] ?: source.function, - ) - is GoIRBuiltinValueExpr -> source.copy(type = type(source.type)) - is GoIRFreeVarValueExpr -> source.copy(type = type(source.type)) - } - - fun instruction(source: GoIRInst): GoIRInst { - val location = location(source.location) - return when (source) { - is GoIRAssignInst -> GoIRAssignInst(location, registers.getValue(source.register), expression(source.expr)) - is GoIRPhi -> GoIRPhi( - location, - registers.getValue(source.register), - source.edges.mapValues { value(it.value) }, - source.comment, - ) - is GoIRCall -> GoIRCall(location, registers.getValue(source.register), call(source.call)) - is GoIRJump -> source.copy(location = location) - is GoIRIf -> source.copy(location = location, cond = value(source.cond)) - is GoIRReturn -> source.copy(location = location, results = source.results.map(::value)) - is GoIRPanic -> source.copy(location = location, x = value(source.x)) - is GoIRFieldStore -> source.copy( - location = location, - addr = value(source.addr), - base = value(source.base), - value = value(source.value), - ) - is GoIRIndexStore -> source.copy( - location = location, - addr = value(source.addr), - base = value(source.base), - index = value(source.index), - value = value(source.value), - ) - is GoIRStore -> source.copy( - location = location, addr = value(source.addr), value = value(source.value), - ) - is GoIRGlobalStore -> source.copy(location = location, value = value(source.value)) - is GoIRMapUpdate -> source.copy( - location = location, - map = value(source.map), - key = value(source.key), - value = value(source.value), - ) - is GoIRSend -> source.copy(location = location, chan = value(source.chan), x = value(source.x)) - is GoIRGo -> source.copy(location = location, call = call(source.call)) - is GoIRDefer -> source.copy(location = location, call = call(source.call)) - is GoIRRunDefers -> source.copy(location = location) - is GoIRDebugRef -> source.copy(location = location, x = value(source.x)) - } - } -} - -private const val MODEL_KIND_FOR_SPECIALIZER = "opentaint model" -private const val MODEL_SUPPORT_KIND_FOR_SPECIALIZER = "opentaint model support" diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt new file mode 100644 index 0000000000..dee6a6631f --- /dev/null +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt @@ -0,0 +1,205 @@ +package org.opentaint.ir.go.client + +import org.opentaint.ir.go.api.* +import org.opentaint.ir.go.type.* +import java.util.IdentityHashMap + +/** A lazy, cached IR view. Every reference exposed by the view uses the same transformer chain. */ +class GoIRTransformerChain( + private val source: GoIRProgram, + private val transformers: List, +) : GoIRProgram { + override val originalProgram: GoIRProgram get() = source.originalProgram + private val types = LookupTypes(this, source.anonymousInterfaces) + private val functionCache = IdentityHashMap() + private val globalCache = IdentityHashMap() + private val packageCache = mutableMapOf() + private val additionalPackages by lazy { + transformers.flatMap { it.additionalPackages }.associateBy { it.importPath } + } + + override val packages: Map by lazy { + (source.packages.keys + additionalPackages.keys).associateWith { findPackage(it)!! } + } + override val anonymousInterfaces: Map + get() = types.anonymousInterfaces + + override fun findPackage(importPath: String): GoIRPackage? = synchronized(packageCache) { + packageCache[importPath]?.let { return it } + val original = source.findPackage(importPath) ?: additionalPackages[importPath] ?: return null + val transformed = transformers.fold(original) { pkg, transformer -> transformer.transformPackage(pkg) } + PackageView(transformed).also { packageCache[importPath] = it } + } + + override fun allFunctions(): List = packages.values.flatMap { it.functions } + override fun allNamedTypes(): List = packages.values.flatMap { it.namedTypes } + override fun mainPackage(): GoIRPackage? = packages.values.find { it.name == "main" } + + private fun function(source: GoIRFunction): GoIRFunction = synchronized(functionCache) { + functionCache[source]?.let { return it } + val transformed = transformers.fold(source) { fn, transformer -> transformer.transformFunction(fn) } + functionCache[transformed]?.let { functionCache[source] = it; return it } + val view = when (transformed) { + is GoIRApproximatedFunction -> ApproximationView(transformed) + is GoIRModelFunction -> ModelFunctionView(transformed) + else -> FunctionView(transformed) + } + functionCache[source] = view + functionCache[transformed] = view + view + } + + private fun global(source: GoIRGlobal): GoIRGlobal = synchronized(globalCache) { + globalCache[source]?.let { return it } + val resolved = (findPackage(source.pkg.importPath) as? PackageView) + ?.source?.findGlobal(source.name) ?: source + globalCache[resolved]?.let { globalCache[source] = it; return it } + val view = object : GoIRGlobal by resolved { + override val pkg get() = findPackage(resolved.pkg.importPath)!! + override val type by lazy { types(resolved.type) } + } + globalCache[source] = view + globalCache[resolved] = view + view + } + + private fun reference(ref: GoIrFunctionReference): GoIrFunctionReference = + ref.copy(signature = types(ref.signature) as GoIRFuncType).also { it.program = this } + + private open inner class FunctionView(protected val source: GoIRFunction) : GoIRFunction by source { + override val pkg: GoIRPackage? get() = source.pkg?.importPath?.let(::findPackage) + override val signature by lazy { types(source.signature) as GoIRFuncType } + override val params by lazy { source.params.map { it.copy(type = types(it.type)) } } + override val freeVars by lazy { source.freeVars.map { it.copy(type = types(it.type)) } } + override val typeParams by lazy { source.typeParams.map { it.copy(constraint = types(it.constraint)) } } + override val typeArgs by lazy { source.typeArgs.map(types::invoke) } + override val receiverType get() = signature.recv + override val parent by lazy { source.parent?.let(::reference) } + override val anonymousFunctions by lazy { source.anonymousFunctions.map(::reference) } + override val body by lazy { source.body?.let { transformGoIRBody(it, this, types, ::function, ::global) } } + override val hasBody get() = source.hasBody + override val bodyAvailable get() = source.bodyAvailable + override val parameters get() = if (freeVars.isEmpty()) params + else params + freeVars.map { GoIRParameter(it.name, it.type, params.size + it.index) } + override val returnType get() = signature.results.first() + override fun flowGraph() = body?.instGraph ?: error("Function $name has no body") + override fun equals(other: Any?): Boolean = other is GoIRFunction && fullName == other.fullName + override fun hashCode(): Int = fullName.hashCode() + override fun toString(): String = source.toString() + } + + private inner class ModelFunctionView(source: GoIRModelFunction) : FunctionView(source), GoIRModelFunction + private inner class ApproximationView(source: GoIRApproximatedFunction) : + FunctionView(source), GoIRApproximatedFunction { + override val original: GoIRFunction = source.original + } + + private inner class PackageView(val source: GoIRPackage) : GoIRPackage by source { + override val functions by lazy { source.functions.map(::function) } + override val namedTypes by lazy { source.namedTypes.map { NamedTypeView(it, this) } } + override val globals by lazy { source.globals.map(::global) } + override val constants by lazy { + source.constants.map { constant -> + object : GoIRConst by constant { + override val pkg get() = this@PackageView + override val type by lazy { types(constant.type) } + } + } + } + override fun findGlobal(name: String) = globals.find { it.name == name } + override fun findConstant(name: String) = constants.find { it.name == name } + override val imports by lazy { source.imports.map { findPackage(it.importPath) ?: it } } + override val initFunction get() = source.initFunction?.let(::function) + override fun findNamedType(name: String) = namedTypes.find { it.name == name } + override fun allMethods() = namedTypes.flatMap { it.allMethods() } + } + + private inner class NamedTypeView(private val source: GoIRNamedType, override val pkg: GoIRPackage) : + GoIRNamedType by source { + override val underlying by lazy { types(source.underlying) } + override val fields by lazy { source.fields.map { it.copy(type = types(it.type), enclosingType = this) } } + override val methods by lazy { source.methods.map(::function) } + override val pointerMethods by lazy { source.pointerMethods.map(::function) } + override val typeParams by lazy { source.typeParams.map { it.copy(constraint = types(it.constraint)) } } + override val interfaceMethods by lazy { + source.interfaceMethods.map { it.copy(signature = types(it.signature) as GoIRFuncType, enclosingInterface = this) } + } + override val embeddedInterfaces by lazy { + source.embeddedInterfaces.map { findPackage(it.pkg.importPath)?.findNamedType(it.name) ?: it } + } + override fun allMethods() = methods + pointerMethods + override fun methodByName(name: String) = allMethods().find { it.name == name } + } +} + +private class LookupTypes( + private val program: GoIRProgram, + originalInterfaces: Map, +) : (GoIRType) -> GoIRType { + private val memo = IdentityHashMap() + + // Anonymous-interface IDs are local to each deserialized program. Intern structural + // definitions so model-only interfaces cannot collide with program interface IDs. + private val interfaces = originalInterfaces.toMutableMap() + private val interfaceIds = mutableMapOf().apply { + originalInterfaces.forEach { (id, type) -> putIfAbsent(type, id) } + } + private var nextInterfaceId = (originalInterfaces.keys.maxOrNull() ?: -1) + 1 + + val anonymousInterfaces: Map = + object : AbstractMap() { + override fun get(key: Int): GoIRAnonymousInterfaceType? = synchronized(memo) { + interfaces[key]?.let { invoke(it) as GoIRAnonymousInterfaceType } + } + override val entries: Set> + get() = synchronized(memo) { + interfaces.toMap().mapValues { invoke(it.value) as GoIRAnonymousInterfaceType }.entries + } + } + + private fun interfaceId(type: GoIRAnonymousInterfaceType): Int = interfaceIds.getOrPut(type) { + nextInterfaceId++.also { interfaces[it] = type } + } + + private fun reference(ref: NamedTypeRef) = ref.copy().also { it.program = program } + + override fun invoke(type: GoIRType): GoIRType = synchronized(memo) { + memo[type]?.let { return it } + val result = when (type) { + is GoIRTypeParamType -> type.copy(constraint = invoke(type.constraint)) + is GoIRTypeParamRef -> type + is GoIRPointerType -> GoIRPointerType(invoke(type.elem)) + is GoIRArrayType -> GoIRArrayType(invoke(type.elem), type.length) + is GoIRSliceType -> GoIRSliceType(invoke(type.elem)) + is GoIRMapType -> GoIRMapType(invoke(type.key), invoke(type.value)) + is GoIRChanType -> GoIRChanType(invoke(type.elem), type.direction) + is GoIRStructType -> GoIRStructType( + type.fields.map { GoIRStructField(it.name, invoke(it.type), it.isEmbedded, it.tag) }, + type.structTypeRef?.let(::reference), + ) + is GoIRNamedTypeRef -> GoIRNamedTypeRef(reference(type.typeRef), type.typeArgs.map(::invoke)) + is GoIRNamedInterfaceType -> GoIRNamedInterfaceType( + type.methods.map { GoIRInterfaceMethodSig(it.name, invoke(it.signature) as GoIRFuncType) }, + type.embeds.map(::invoke), + reference(type.interfaceTypeRef), + ) + is GoIRAnonymousInterfaceTypeRef -> GoIRAnonymousInterfaceTypeRef(interfaceId(type.interfaceType)) + .also { it.program = program } + is GoIRAnonymousInterfaceType -> GoIRAnonymousInterfaceType( + interfaceId(type), + type.methods.map { GoIRInterfaceMethodSig(it.name, invoke(it.signature) as GoIRFuncType) }, + type.embeds.map(::invoke), + ) + is GoIRFuncType -> GoIRFuncType( + type.params.map(::invoke), + type.results.map(::invoke), + type.isVariadic, + type.recv?.let(::invoke), + ) + is GoIRTupleType -> GoIRTupleType(type.elements.map(::invoke)) + else -> type + } + memo[type] = result + result + } +} diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt index 9196c26163..b24d2d61d3 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt @@ -23,8 +23,8 @@ class GoIRFunctionImpl( override val isMethod: Boolean, override val isPointerReceiver: Boolean, override val isExported: Boolean, - isSynthetic: Boolean, - syntheticKind: String?, + override val isSynthetic: Boolean, + override val syntheticKind: String?, private val declaredHasBody: Boolean = false, override val parent: GoIrFunctionReference?, override val anonymousFunctions: List, @@ -33,18 +33,8 @@ class GoIRFunctionImpl( override val typeArgs: List = emptyList(), ) : GoIRFunction { private var owner = pkg - private var synthetic = isSynthetic - private var kind = syntheticKind - - override val isSynthetic: Boolean get() = synthetic - override val syntheticKind: String? get() = kind override val pkg: GoIRPackage? get() = owner - internal fun markAsModel(kind: String) { - synthetic = true - this.kind = kind - } - internal fun rebindPackage(pkg: GoIRPackage) { owner = pkg } diff --git a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt index be51fa2532..6c2efc0c4c 100644 --- a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt +++ b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt @@ -4,18 +4,29 @@ import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import org.opentaint.ir.go.api.GoIRApproximatedFunction +import org.opentaint.ir.go.api.GoIRModelFunction +import org.opentaint.ir.go.api.GoIRFunction +import org.opentaint.ir.go.api.GoIRTransformer +import org.opentaint.ir.go.client.GoIRTransformerChain import org.opentaint.ir.go.cfg.GoIRCallTarget import org.opentaint.ir.go.client.GoIRLoadConfig import org.opentaint.ir.go.client.GoIRLoadMode +import org.opentaint.ir.go.expr.GoIRFunctionValueExpr import org.opentaint.ir.go.expr.GoIRFieldAddrExpr import org.opentaint.ir.go.expr.GoIRMakeClosureExpr import org.opentaint.ir.go.ext.findExpressions import org.opentaint.ir.go.ext.findInstructions import org.opentaint.ir.go.inst.GoIRCall +import org.opentaint.ir.go.inst.GoIRGo +import org.opentaint.ir.go.inst.GoIRDefer import org.opentaint.ir.go.inst.GoIRGlobalStore import org.opentaint.ir.go.inst.GoIRReturn import org.opentaint.ir.go.test.GoIRTestBuilder import org.opentaint.ir.go.test.GoIRTestExtension +import org.opentaint.ir.go.type.GoIRAnonymousInterfaceType +import org.opentaint.ir.go.type.GoIRNamedTypeRef +import org.opentaint.ir.go.type.GoIRPointerType import org.opentaint.ir.go.type.GoIRStructType import org.opentaint.ir.go.value.GoIRConstValue import org.opentaint.ir.go.value.GoIRConstantValue @@ -30,7 +41,14 @@ class ModelTests { val project = Files.createTempDirectory("goir-model-project") project.resolve("go.mod").write("module example.com/app\ngo 1.22\n") project.resolve("main.go").write( - "package main\nimport \"strings\"\nfunc Upper(value string) string { return strings.ToUpper(value) }\n", + """ + package main + import "strings" + func Upper(value string) string { return strings.ToUpper(value) } + func Start(value string) { go strings.ToUpper(value); defer strings.ToUpper(value) } + func Function() func(string) string { return strings.ToUpper } + func Closure(value string) func() string { return func() string { return strings.ToUpper(value) } } + """.trimIndent(), ) val model = Files.createTempDirectory("goir-model") @@ -51,10 +69,29 @@ class ModelTests { val caller = program.findPackage("example.com/app")!!.functions.single { it.name == "Upper" } val originalTarget = caller.findInstructions().single().call.target as GoIRCallTarget.Function assertThat(function.bodyAvailable).isTrue() - assertThat(program.effectiveFunction(originalTarget.function)).isSameAs(function) + assertThat(originalTarget.function).isSameAs(function) + assertThat(function).isInstanceOf(GoIRApproximatedFunction::class.java) val returned = (function.body!!.blocks.single().terminator as GoIRReturn).results.single() as GoIRParameterValue assertThat(returned.paramIndex).isZero() assertThat(returned.type).isEqualTo(function.params.single().type) + val pkg = caller.pkg!! + val start = pkg.functions.single { it.name == "Start" } + val goTarget = start.findInstructions().single().call.target as GoIRCallTarget.Function + val deferTarget = start.findInstructions().single().call.target as GoIRCallTarget.Function + assertThat(goTarget.function).isSameAs(function) + assertThat(deferTarget.function).isSameAs(function) + val value = pkg.functions.single { it.name == "Function" }.findExpressions().single() + assertThat(value.function).isSameAs(function) + val closure = pkg.functions.single { it.name == "Closure" }.anonymousFunctions.single().function + val closureTarget = closure.findInstructions().single().call.target as GoIRCallTarget.Function + assertThat(closureTarget.function).isSameAs(function) + assertThat(function.body!!.function).isSameAs(function) + assertThat(caller.body!!.instructions).allMatch { it.location.method === caller } + val originalCaller = program.originalProgram.findPackage(pkg.importPath)!!.functions.single { it.name == "Upper" } + val rawTarget = originalCaller.findInstructions().single().call.target as GoIRCallTarget.Function + assertThat(rawTarget.function).isSameAs((function as GoIRApproximatedFunction).original) + assertThat(rawTarget.function).isNotSameAs(function) + } @Test @@ -80,7 +117,8 @@ class ModelTests { assertThat(originalProgram).isNotSameAs(effectiveProgram) assertThat(effectiveIdentity.pkg).isSameAs(effectivePackage) - assertThat(effectiveIdentity.syntheticKind).isEqualTo("opentaint model") + assertThat(effectiveIdentity).isInstanceOf(GoIRApproximatedFunction::class.java) + assertThat((effectiveIdentity as GoIRApproximatedFunction).original).isSameAs(originalIdentity) assertThat(effectiveIdentity.findInstructions()) .anyMatch { it.call.target?.displayName == "example.com/original.helper" } assertThat(originalIdentity.isSynthetic).isFalse() @@ -133,6 +171,11 @@ class ModelTests { assertThat(box.pointerMethods.map { it.name }).contains("Put", "Keep", "Helper") val put = box.pointerMethods.single { it.name == "Put" } + val receiverType = (put.params.first().type as GoIRPointerType).elem as GoIRNamedTypeRef + assertThat(receiverType.namedType).isSameAs(box) + assertThat(receiverType.namedType.pointerMethods.single { it.name == "Put" }).isSameAs(put) + assertThat(program.originalProgram.findPackage(targetPackage.importPath)!!.findNamedType("Box")!!.fields.map { it.name }) + .containsExactly("Original") val shadowAccess = put.findExpressions().single { it.fieldName == "Shadow" } assertThat(shadowAccess.fieldIndex).isEqualTo(1) assertThat(box.pointerMethods.single { it.name == "Keep" }.bodyAvailable).isTrue() @@ -624,7 +667,7 @@ class ModelTests { assertThat(targetPackage.findGlobal("AddedGlobal")).isNotNull() assertThat(targetPackage.findConstant("AddedConstant")).isNotNull() assertThat(helper.isSynthetic).isTrue() - assertThat(helper.syntheticKind).isEqualTo("opentaint model support") + assertThat(helper).isInstanceOf(GoIRModelFunction::class.java) assertThat(helper.bodyAvailable).isTrue() assertThat(calledFunction.function).isSameAs(helper) } @@ -681,6 +724,55 @@ class ModelTests { } } + @Test + fun `model anonymous interface definitions retain their own lookup scope`(builder: GoIRTestBuilder) { + val project = project( + "example.com/interfaces", + """ + package interfaces + func Original(value interface { Number() int }) int { return value.Number() } + func Target(value string) string { return value } + """.trimIndent(), + ) + val model = model( + "example.com/interfaces", + """ + package interfaces + type Box struct { value string } + func (b Box) Text() string { return b.value } + func helper(input interface { Text() string }) string { return input.Text() } + func Target(value string) string { return helper(Box{value}) } + """.trimIndent(), + ) + val program = builder.buildFromDir(project, modelConfig(model)) + val functions = program.findPackage("example.com/interfaces")!!.functions + val modelType = functions.single { it.name == "helper" }.signature.params.single() as GoIRAnonymousInterfaceType + val originalType = functions.single { it.name == "Original" }.signature.params.single() as GoIRAnonymousInterfaceType + assertThat(modelType.methods.map { it.name }).containsExactly("Text") + assertThat(originalType.methods.map { it.name }).containsExactly("Number") + assertThat(program.anonymousInterfaces[modelType.id]).isEqualTo(modelType) + } + + @Test + fun `transformers compose in order and resolve calls through one cached view`(builder: GoIRTestBuilder) { + val original = builder.buildFromSource("package p\nfunc Target() {}\nfunc Caller() { Target() }\n") + fun suffix(value: String) = object : GoIRTransformer { + override fun transformFunction(function: GoIRFunction): GoIRFunction = + object : GoIRFunction by function { + override val syntheticKind = function.syntheticKind.orEmpty() + value + } + } + val transformed = GoIRTransformerChain(original, listOf(suffix("first"), suffix("second"))) + val pkg = transformed.packages.getValue("test/p") + val target = pkg.functions.single { it.name == "Target" } + val caller = pkg.functions.single { it.name == "Caller" } + assertThat(target.syntheticKind).isEqualTo("firstsecond") + val call = caller.findInstructions().single().call.target as GoIRCallTarget.Function + assertThat(call.function).isSameAs(target) + assertThat(transformed.allFunctions()).contains(target, caller) + assertThat(original.findPackage("test/p")!!.functions.single { it.name == "Target" }.syntheticKind).isNull() + } + private fun project(module: String, source: String): Path { val project = Files.createTempDirectory("goir-model-project") project.resolve("go.mod").write("module $module\ngo 1.22\n") diff --git a/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/Approximations.kt b/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/Approximations.kt index be4854dd57..2eef10ad54 100644 --- a/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/Approximations.kt +++ b/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/Approximations.kt @@ -24,7 +24,6 @@ import org.opentaint.ir.api.storage.ers.EntityIterable import org.opentaint.ir.api.storage.ers.compressed import org.opentaint.ir.approximation.annotation.Approximate import org.opentaint.ir.approximation.annotation.ApproximateByName -import org.opentaint.ir.approximation.annotation.ApproximatedClassName import org.opentaint.ir.approximation.annotation.ApproximatedFieldName import org.opentaint.ir.approximation.annotation.ApproximatedMethodName import org.opentaint.ir.approximation.annotation.Version @@ -99,7 +98,6 @@ class Approximations( val persistence = signal.jIRdb.persistence val approxSymbolClass = persistence.findSymbolId(approximationAnnotationClassName) val approxSymbolName = persistence.findSymbolId(approximationByNameAnnotationClassName) - val patchedClassSymbolName = persistence.findSymbolId(approximatedClassName) persistence.read { context -> context.execute( sqlAction = { @@ -149,24 +147,7 @@ class Approximations( clazz.getCompressed("nameId") to annotationValue.getCompressedBlob("classSymbolId") } - val patchedClassApprox = context.txn.find("Annotation", "nameId", patchedClassSymbolName.compressed) - .filter { it.getCompressedBlob("refKind") == RefKind.CLASS.ordinal } - .mapNotNull { annotation -> - matchAnnotationVersions(persistence, annotation, versionsId, versionSymbol, targetId, fromVersionId, toVersionId) - }.mapNotNull { (annotation, values) -> - val clazz = annotation.getLink("ref") - val approximationNameId = clazz.getCompressed("nameId") - ?: return@mapNotNull null - val approximationName = persistence.findSymbolName(approximationNameId) - val patchedName = values.annotationStringValueByName(persistence, valueId) - ?: return@mapNotNull null - val originalName = prefixAddressedTarget(approximationName, patchedName) - ?: error("@$approximatedClassName requires an $approximationPackagePrefix package") - - approximationNameId to persistence.findSymbolId(originalName) - } - - val explicitMappings = (namedApprox + classApprox + patchedClassApprox).toList() + val explicitMappings = (namedApprox + classApprox).toList() val explicitlyMappedClasses = explicitMappings.mapTo(hashSetOf()) { it.first } val prefixMappings = context.txn.all("Class").mapNotNull { clazz -> val approximationNameId = clazz.getCompressed("nameId") @@ -396,8 +377,7 @@ private class ApproximationIndexer( val prefixTarget = prefixAddressedTarget(approximationClassName) val approximationAnnotations = classNode.visibleAnnotations.orEmpty().filter { approximationAnnotationClassName in it.desc.className || - approximationByNameAnnotationClassName in it.desc.className || - approximatedClassName in it.desc.className + approximationByNameAnnotationClassName in it.desc.className } if (prefixTarget == null && approximationAnnotations.isEmpty()) return if (approximationAnnotations.size > 1) { @@ -412,17 +392,6 @@ private class ApproximationIndexer( val originalClassName = when { approximationAnnotation == null -> prefixTarget!!.toOriginalName() - approximatedClassName in approximationAnnotation.desc.className -> { - val patchedName = annotationStringValueByName(approximationAnnotation.values, "value") - ?: error("unable to find `value` in @$approximatedClassName") - val target = prefixAddressedTarget(approximationClassName, patchedName) - if (target == null) { - logger.error { "@$approximatedClassName on $approximationClassName requires an $approximationPackagePrefix package" } - return - } - target.toOriginalName() - } - approximationByNameAnnotationClassName in approximationAnnotation.desc.className -> { val target = annotationStringValueByName(approximationAnnotation.values, "value") ?: error("unable to find `value` in @$approximationByNameAnnotationClassName") @@ -476,24 +445,15 @@ private class ApproximationIndexer( private val approximationAnnotationClassName = Approximate::class.qualifiedName!! private val approximationByNameAnnotationClassName = ApproximateByName::class.qualifiedName!! -private val approximatedClassName = ApproximatedClassName::class.qualifiedName!! private val approximatedMethodName = ApproximatedMethodName::class.qualifiedName!! private val approximatedFieldName = ApproximatedFieldName::class.qualifiedName!! private val versionAnnotationClassName = Version::class.qualifiedName!! private const val approximationPackagePrefix = "opentaint." -private fun prefixAddressedTarget(approximationClassName: String, patchedClassName: String? = null): String? { - if (!approximationClassName.startsWith(approximationPackagePrefix)) return null - val defaultTarget = approximationClassName.removePrefix(approximationPackagePrefix) - if (defaultTarget.isEmpty()) return null - if (patchedClassName == null) return defaultTarget - require(patchedClassName.isNotBlank() && '.' !in patchedClassName) { - "Approximated class name must be one non-empty JVM class-name component: $patchedClassName" - } - val targetPackage = defaultTarget.substringBeforeLast('.', missingDelimiterValue = "") - return if (targetPackage.isEmpty()) patchedClassName else "$targetPackage.$patchedClassName" -} +private fun prefixAddressedTarget(approximationClassName: String): String? = + approximationClassName.takeIf { it.startsWith(approximationPackagePrefix) } + ?.removePrefix(approximationPackagePrefix)?.takeIf { it.isNotEmpty() } @JvmInline value class ApproximationClassName(val className: String) { diff --git a/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/annotation/ApproximationAnnotations.kt b/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/annotation/ApproximationAnnotations.kt index 3656a722df..24d73bdaff 100644 --- a/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/annotation/ApproximationAnnotations.kt +++ b/core/opentaint-ir/opentaint-ir-approximations/src/main/kotlin/org/opentaint/ir/approximation/annotation/ApproximationAnnotations.kt @@ -20,17 +20,6 @@ annotation class ApproximateByName( val versions: Array = [], ) -/** - * Sets the final JVM class name for a prefix-addressed approximation. - * The `opentaint.` package sets the target package. - */ -@Suppress("unused") -@Target(AnnotationTarget.CLASS) -annotation class ApproximatedClassName( - val value: String, - val versions: Array = [], -) - @Suppress("unused") @Target(AnnotationTarget.FUNCTION) annotation class ApproximatedMethodName(val value: String) diff --git a/core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/org/opentaint/ir/approximations/target/KotlinClassName.java b/core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/org/opentaint/ir/approximations/target/KotlinClassName.java index 9ccdf15030..faa32b0790 100644 --- a/core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/org/opentaint/ir/approximations/target/KotlinClassName.java +++ b/core/opentaint-ir/opentaint-ir-approximations/src/test/java/opentaint/org/opentaint/ir/approximations/target/KotlinClassName.java @@ -1,8 +1,8 @@ package opentaint.org.opentaint.ir.approximations.target; -import org.opentaint.ir.approximation.annotation.ApproximatedClassName; +import org.opentaint.ir.approximation.annotation.ApproximateByName; -@ApproximatedClassName("Kotlin-Class") +@ApproximateByName("org.opentaint.ir.approximations.target.Kotlin-Class") public class KotlinClassName { public int evaluate(int value) { return value; diff --git a/core/src/main/kotlin/org/opentaint/go/sast/dataflow/GoUnitResolver.kt b/core/src/main/kotlin/org/opentaint/go/sast/dataflow/GoUnitResolver.kt index a68e5861d5..a432e0782a 100644 --- a/core/src/main/kotlin/org/opentaint/go/sast/dataflow/GoUnitResolver.kt +++ b/core/src/main/kotlin/org/opentaint/go/sast/dataflow/GoUnitResolver.kt @@ -4,16 +4,14 @@ import org.opentaint.dataflow.ifds.UnitResolver import org.opentaint.dataflow.ifds.UnitType import org.opentaint.dataflow.ifds.UnknownUnit import org.opentaint.ir.go.api.GoIRFunction +import org.opentaint.ir.go.api.GoIRModelFunction class GoUnitResolver : UnitResolver { data class GoPackageUnit(val pkgImportPath: String) : UnitType override fun resolve(method: GoIRFunction): UnitType { val pkg = method.pkg ?: return UnknownUnit - return if ( - pkg.isProject || method.syntheticKind == "opentaint model" || - method.syntheticKind == "opentaint model support" - ) { + return if (pkg.isProject || method is GoIRModelFunction) { GoPackageUnit(pkg.importPath) } else { UnknownUnit diff --git a/core/src/test/kotlin/org/opentaint/go/sast/dataflow/GoUnitResolverTest.kt b/core/src/test/kotlin/org/opentaint/go/sast/dataflow/GoUnitResolverTest.kt index 0782671cf4..c8cf8bb050 100644 --- a/core/src/test/kotlin/org/opentaint/go/sast/dataflow/GoUnitResolverTest.kt +++ b/core/src/test/kotlin/org/opentaint/go/sast/dataflow/GoUnitResolverTest.kt @@ -2,6 +2,8 @@ package org.opentaint.go.sast.dataflow import org.junit.jupiter.api.Test import org.opentaint.dataflow.ifds.UnknownUnit +import org.opentaint.ir.go.api.GoIRModelFunction +import org.opentaint.ir.go.api.GoIRApproximatedFunction import org.opentaint.ir.go.api.GoIRConst import org.opentaint.ir.go.api.GoIRFreeVar import org.opentaint.ir.go.api.GoIRFunction @@ -42,6 +44,19 @@ class GoUnitResolverTest { assertEquals(UnknownUnit, resolver.resolve(fn)) } + @Test + fun `typed models are analyzed in dependency and standard library packages`() { + val resolver = GoUnitResolver() + val dependency = fakeFunction(fakePackage("example.com/lib", isStdlib = false, isDependency = true)) + val helper = object : GoIRModelFunction, GoIRFunction by dependency {} + assertEquals(GoUnitResolver.GoPackageUnit("example.com/lib"), resolver.resolve(helper)) + val stdlib = fakeFunction(fakePackage("strings", isStdlib = true, isDependency = false)) + val approximation = object : GoIRApproximatedFunction, GoIRFunction by stdlib { + override val original = stdlib + } + assertEquals(GoUnitResolver.GoPackageUnit("strings"), resolver.resolve(approximation)) + } + private fun fakePackage(importPath: String, isStdlib: Boolean, isDependency: Boolean): GoIRPackage = object : GoIRPackage { override val importPath = importPath From 773bdeb40d32d30dacd6079ae9f171b8bcaf9aa6 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Thu, 22 Oct 2026 22:53:49 +0200 Subject: [PATCH 09/12] refactor(go-models): resolve symbolic IR lookups in context Address review 5615028707 by moving model path normalization into an IR transformer, making package ownership immutable, and replacing body copying with decoding in the lookup context. Remove transformation caches and add lookup interception regressions. Validation: 131 Go IR tests and 5 analyzer tests passed; affected consumers compile; git diff --check passes. Benchmark and roundtrip tasks excluded. --- .../org/opentaint/ir/go/api/GoIRLookup.kt | 30 +++ .../org/opentaint/ir/go/api/GoIRProgram.kt | 2 + .../opentaint/ir/go/api/GoIRTransformer.kt | 8 +- .../ir/go/api/GoIrFunctionReference.kt | 17 +- .../org/opentaint/ir/go/cfg/GoIRCallInfo.kt | 7 +- .../org/opentaint/ir/go/expr/GoIRExpr.kt | 9 +- .../org/opentaint/ir/go/inst/GoIRInst.kt | 3 +- .../org/opentaint/ir/go/type/GoIRType.kt | 2 +- .../opentaint/ir/go/client/GoIRBodyReader.kt | 17 ++ .../ir/go/client/GoIRBodyTransformer.kt | 198 ------------------ .../ir/go/client/GoIRDeserializer.kt | 143 +++++++------ .../opentaint/ir/go/client/GoIRModelLoader.kt | 8 +- .../ir/go/client/GoIRModelOverlay.kt | 97 +++------ .../ir/go/client/GoIRModelSpecializer.kt | 30 +-- .../ir/go/client/GoIRTransformerChain.kt | 137 +++++++----- .../ir/go/client/ModelPathNormalizer.kt | 19 +- .../opentaint/ir/go/impl/GoIRFunctionImpl.kt | 23 +- .../opentaint/ir/go/impl/GoIRGlobalImpl.kt | 16 +- .../opentaint/ir/go/impl/GoIRNamedTypeImpl.kt | 8 +- .../ir/go/test/features/ModelTests.kt | 64 +++++- 20 files changed, 378 insertions(+), 460 deletions(-) create mode 100644 core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRLookup.kt create mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyReader.kt delete mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyTransformer.kt diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRLookup.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRLookup.kt new file mode 100644 index 0000000000..ba25481903 --- /dev/null +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRLookup.kt @@ -0,0 +1,30 @@ +package org.opentaint.ir.go.api + +import org.opentaint.ir.go.type.NamedTypeRef + +/** Symbol resolution in one program view. References cache only their own lookup result. */ +interface GoIRLookup { + fun function(reference: GoIrFunctionReference): GoIRFunction? + fun namedType(reference: NamedTypeRef): GoIRNamedType? + fun global(reference: GoIRGlobalReference): GoIRGlobal? +} + +class GoIRProgramLookup(private val program: GoIRProgram) : GoIRLookup { + override fun function(reference: GoIrFunctionReference): GoIRFunction? = + program.findPackage(reference.pkg)?.functions?.firstOrNull { + it.name == reference.name && it.signature == reference.signature + } + override fun namedType(reference: NamedTypeRef): GoIRNamedType? = + program.findPackage(reference.refTypePkg)?.findNamedType(reference.refTypeName) + override fun global(reference: GoIRGlobalReference): GoIRGlobal? = + program.findPackage(reference.pkg)?.findGlobal(reference.name) +} + +data class GoIRGlobalReference(val pkg: String, val name: String) { + lateinit var program: GoIRProgram + val global: GoIRGlobal by lazy { + program.lookup.global(this) ?: error("Could not find global $pkg.$name") + } +} + +fun GoIRGlobal.reference(): GoIRGlobalReference = GoIRGlobalReference(pkg.importPath, name) diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt index 8b98251d78..accb4add9c 100644 --- a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt @@ -9,6 +9,8 @@ interface GoIRProgram { /** The program before model lookup overlays. This program is [this] when no models are active. */ val originalProgram: GoIRProgram get() = this + val lookup: GoIRLookup get() = GoIRProgramLookup(this) + val packages: Map // keyed by import path val anonymousInterfaces: Map diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRTransformer.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRTransformer.kt index 0715842c1d..72b8f7783d 100644 --- a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRTransformer.kt +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRTransformer.kt @@ -1,8 +1,10 @@ package org.opentaint.ir.go.api -/** Lookup transformations, applied in list order before the resulting IR is exposed. */ +/** A lookup interceptor. Unhandled requests are passed to the next resolver. */ interface GoIRTransformer { - fun transformPackage(pkg: GoIRPackage): GoIRPackage = pkg - fun transformFunction(function: GoIRFunction): GoIRFunction = function + fun resolvePackage(importPath: String, next: () -> GoIRPackage?): GoIRPackage? = next() + fun resolveFunction(reference: GoIrFunctionReference, next: () -> GoIRFunction?): GoIRFunction? = next() + fun packagePath(importPath: String): String = importPath + fun symbolName(fullName: String): String = fullName val additionalPackages: List get() = emptyList() } diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIrFunctionReference.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIrFunctionReference.kt index b006236caa..c9596b1eb1 100644 --- a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIrFunctionReference.kt +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIrFunctionReference.kt @@ -11,14 +11,11 @@ data class GoIrFunctionReference( lateinit var program: GoIRProgram - val function: GoIRFunction - get() { - val p = program.findPackage(pkg) - ?: error("Could not find package $pkg for $fullName") - // Match on name + signature because methods on different named - // types legitimately share short names (e.g. `Dog.Legs` and - // `Snake.Legs`); only the signature's receiver disambiguates them. - return p.functions.firstOrNull { it.name == name && it.signature == signature } - ?: error("Could not find function $fullName with signature $signature") - } + val function: GoIRFunction by lazy { + program.lookup.function(this) + ?: error("Could not find function $fullName with signature $signature") + } } + +fun GoIRFunction.reference(): GoIrFunctionReference = + GoIrFunctionReference(pkg?.importPath.orEmpty(), name, signature) diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/cfg/GoIRCallInfo.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/cfg/GoIRCallInfo.kt index 10940e2780..d435696623 100644 --- a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/cfg/GoIRCallInfo.kt +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/cfg/GoIRCallInfo.kt @@ -1,6 +1,6 @@ package org.opentaint.ir.go.cfg -import org.opentaint.ir.go.api.GoIRFunction +import org.opentaint.ir.go.api.GoIrFunctionReference import org.opentaint.ir.go.api.GoIRPosition import org.opentaint.ir.go.type.GoIRCallMode import org.opentaint.ir.go.type.GoIRChanDirection @@ -11,8 +11,9 @@ sealed interface GoIRCallTarget { val type: GoIRType val displayName: String - data class Function(val function: GoIRFunction) : GoIRCallTarget { - override val type: GoIRType get() = function.signature + data class Function(val lookup: GoIrFunctionReference) : GoIRCallTarget { + val function get() = lookup.function + override val type: GoIRType get() = lookup.signature override val displayName: String get() = function.fullName } diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/expr/GoIRExpr.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/expr/GoIRExpr.kt index e6177eda30..43b3178f1b 100644 --- a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/expr/GoIRExpr.kt +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/expr/GoIRExpr.kt @@ -139,9 +139,10 @@ data class GoIRTypeAssertExpr( data class GoIRMakeClosureExpr( override var type: GoIRType, - val fn: GoIRFunction, + val lookup: org.opentaint.ir.go.api.GoIrFunctionReference, val bindings: List, ) : GoIRExpr { + val fn get() = lookup.function override val operands: List get() = bindings override fun accept(visitor: GoIRExprVisitor): T = visitor.visitMakeClosure(this) override fun toString(): String = "makeclosure ${fn.name}[${bindings.joinToString(", ")}]" @@ -310,8 +311,9 @@ data class GoIRExtractExpr( data class GoIRGlobalValueExpr( override var type: GoIRType, - val global: GoIRGlobal, + val lookup: org.opentaint.ir.go.api.GoIRGlobalReference, ) : GoIRExpr { + val global get() = lookup.global override val operands: List get() = emptyList() override fun accept(visitor: GoIRExprVisitor): T = visitor.visitGlobalValue(this) override fun toString(): String = "global ${global.fullName}" @@ -319,8 +321,9 @@ data class GoIRGlobalValueExpr( data class GoIRFunctionValueExpr( override var type: GoIRType, - val function: GoIRFunction, + val lookup: org.opentaint.ir.go.api.GoIrFunctionReference, ) : GoIRExpr { + val function get() = lookup.function override val operands: List get() = emptyList() override fun accept(visitor: GoIRExprVisitor): T = visitor.visitFunctionValue(this) override fun toString(): String = "func ${function.fullName}" diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/inst/GoIRInst.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/inst/GoIRInst.kt index 8b57957561..cd38e1d117 100644 --- a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/inst/GoIRInst.kt +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/inst/GoIRInst.kt @@ -214,9 +214,10 @@ data class GoIRIndexStore( data class GoIRGlobalStore( override val location: GoInstLocation, - val global: GoIRGlobal, + val lookup: org.opentaint.ir.go.api.GoIRGlobalReference, val value: GoIRValue, ) : GoIRInst { + val global get() = lookup.global override val operands: List get() = listOf(value) override fun accept(visitor: GoIRInstVisitor): T = visitor.visitGlobalStore(this) override fun toString(): String = "global ${global.fullName} = $value" diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/type/GoIRType.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/type/GoIRType.kt index e76f55adc6..a94c9d1f43 100644 --- a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/type/GoIRType.kt +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/type/GoIRType.kt @@ -64,7 +64,7 @@ data class NamedTypeRef( val fullRefName: String get() = "$refTypePkg.$refTypeName" val namedType: GoIRNamedType - get() = program.findPackage(refTypePkg)?.findNamedType(refTypeName) + get() = program.lookup.namedType(this) ?: error("Can't find namedType for $fullRefName") override fun toString(): String = "ref($fullRefName)" diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyReader.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyReader.kt new file mode 100644 index 0000000000..39425c4450 --- /dev/null +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyReader.kt @@ -0,0 +1,17 @@ +package org.opentaint.ir.go.client + +import org.opentaint.ir.go.api.* +import org.opentaint.ir.go.type.* + +/** Decodes a body directly in its owner’s lookup context, without an intermediate IR copy. */ +interface GoIRBodyReader { + fun readBody(owner: GoIRFunction, context: GoIRBodyContext): GoIRBody? +} + +/** Composable resolution policy supplied by the program view, independent of wire decoding. */ +open class GoIRBodyContext { + open fun type(type: GoIRType): GoIRType = type + open fun function(reference: GoIrFunctionReference): GoIrFunctionReference = reference + open fun global(reference: GoIRGlobalReference): GoIRGlobalReference = reference + open fun fieldIndex(type: GoIRType, index: Int, name: String): Int = index +} diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyTransformer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyTransformer.kt deleted file mode 100644 index dd55a9f958..0000000000 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyTransformer.kt +++ /dev/null @@ -1,198 +0,0 @@ -package org.opentaint.ir.go.client - -import java.util.IdentityHashMap -import org.opentaint.ir.go.api.GoIRBody -import org.opentaint.ir.go.api.GoIRFunction -import org.opentaint.ir.go.api.GoIRGlobal -import org.opentaint.ir.go.cfg.GoIRCallInfo -import org.opentaint.ir.go.cfg.GoIRCallTarget -import org.opentaint.ir.go.cfg.GoIRSelectState -import org.opentaint.ir.go.expr.* -import org.opentaint.ir.go.impl.GoIRBasicBlockImpl -import org.opentaint.ir.go.impl.GoIRBodyImpl -import org.opentaint.ir.go.inst.* -import org.opentaint.ir.go.type.* -import org.opentaint.ir.go.value.* - -internal fun transformGoIRBody( - source: GoIRBody, - target: GoIRFunction, - type: (GoIRType) -> GoIRType, - functions: (GoIRFunction) -> GoIRFunction, - globals: (GoIRGlobal) -> GoIRGlobal = { it }, -): GoIRBody { - val blocks = source.blocks.map { GoIRBasicBlockImpl(it.index, it.label) } - val recover = source.recoverBlock?.let { blocks[it.index] } - val body = GoIRBodyImpl(target, blocks, recover) - val registers = IdentityHashMap() - source.instructions.filterIsInstance().forEach { definition -> - registers[definition.register] = GoIRRegister( - type(definition.register.type), - definition.register.index, - definition.register.name, - ) - } - val copier = BodyCopier(body, type, functions, globals, registers) - source.blocks.forEach { oldBlock -> - val block = blocks[oldBlock.index] - block.setInstructions(oldBlock.instructions.map(copier::instruction)) - block.predIndices = oldBlock.predecessors.map { it.index } - block.succIndices = oldBlock.successors.map { it.index } - block.idomIndex = oldBlock.idom?.index ?: -1 - block.domineeIndices = oldBlock.dominatedBlocks.map { it.index } - } - blocks.forEach { block -> - block.resolvePredecessors(blocks) - block.resolveSuccessors(blocks) - block.resolveIdom(blocks) - block.resolveDominees(blocks) - } - return body -} - -private class BodyCopier( - private val body: GoIRBody, - private val type: (GoIRType) -> GoIRType, - private val functions: (GoIRFunction) -> GoIRFunction, - private val globals: (GoIRGlobal) -> GoIRGlobal, - private val registers: IdentityHashMap, -) { - private fun value(value: GoIRValue): GoIRValue = when (value) { - is GoIRRegister -> registers[value] ?: error("No copied register ${value.index}") - is GoIRParameterValue -> GoIRParameterValue(type(value.type), value.name, value.paramIndex) - is GoIRConstValue -> GoIRConstValue(type(value.type), value.name, value.value) - else -> error("Unsupported Go IR value ${value::class.simpleName}") - } - - private fun location(source: GoInstLocation) = - GoInstLocation(body, source.index, source.blockIndex, source.position) - - private fun call(source: GoIRCallInfo): GoIRCallInfo = GoIRCallInfo( - mode = source.mode, - target = when (val target = source.target) { - is GoIRCallTarget.Function -> GoIRCallTarget.Function( - functions(target.function), - ) - is GoIRCallTarget.Builtin -> GoIRCallTarget.Builtin(target.name, type(target.type)) - is GoIRCallTarget.Dynamic -> GoIRCallTarget.Dynamic(value(target.value)) - null -> null - }, - receiver = source.receiver?.let(::value), - methodName = source.methodName, - args = source.args.map(::value), - resultType = type(source.resultType), - ) - - private fun expression(source: GoIRExpr): GoIRExpr = when (source) { - is GoIRAllocExpr -> source.copy(type = type(source.type), allocType = type(source.allocType)) - is GoIRBinOpExpr -> source.copy(type = type(source.type), x = value(source.x), y = value(source.y)) - is GoIRUnOpExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRChangeTypeExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRConvertExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRMultiConvertExpr -> source.copy( - type = type(source.type), - x = value(source.x), - fromType = type(source.fromType), - toType = type(source.toType), - ) - is GoIRChangeInterfaceExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRSliceToArrayPointerExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRMakeInterfaceExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRTypeAssertExpr -> source.copy( - type = type(source.type), - x = value(source.x), - assertedType = type(source.assertedType), - ) - is GoIRMakeClosureExpr -> source.copy( - type = type(source.type), - fn = functions(source.fn), - bindings = source.bindings.map(::value), - ) - is GoIRMakeMapExpr -> source.copy(type = type(source.type), reserve = source.reserve?.let(::value)) - is GoIRMakeChanExpr -> source.copy(type = type(source.type), size = value(source.size)) - is GoIRMakeSliceExpr -> source.copy( - type = type(source.type), - len = value(source.len), - cap = value(source.cap), - ) - is GoIRFieldAddrExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRFieldExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRIndexAddrExpr -> source.copy( - type = type(source.type), x = value(source.x), indexValue = value(source.indexValue), - ) - is GoIRIndexExpr -> source.copy( - type = type(source.type), x = value(source.x), indexValue = value(source.indexValue), - ) - is GoIRSliceExpr -> source.copy( - type = type(source.type), - x = value(source.x), - low = source.low?.let(::value), - high = source.high?.let(::value), - max = source.max?.let(::value), - ) - is GoIRLookupExpr -> source.copy( - type = type(source.type), x = value(source.x), indexValue = value(source.indexValue), - ) - is GoIRRangeExpr -> source.copy(type = type(source.type), x = value(source.x)) - is GoIRNextExpr -> source.copy(type = type(source.type), iter = value(source.iter)) - is GoIRSelectExpr -> source.copy( - type = type(source.type), - states = source.states.map { - GoIRSelectState(it.direction, value(it.chan), it.send?.let(::value), it.position) - }, - ) - is GoIRExtractExpr -> source.copy(type = type(source.type), tuple = value(source.tuple)) - is GoIRGlobalValueExpr -> source.copy(type = type(source.type), global = globals(source.global)) - is GoIRFunctionValueExpr -> source.copy( - type = type(source.type), function = functions(source.function), - ) - is GoIRBuiltinValueExpr -> source.copy(type = type(source.type)) - is GoIRFreeVarValueExpr -> source.copy(type = type(source.type)) - } - - fun instruction(source: GoIRInst): GoIRInst { - val location = location(source.location) - return when (source) { - is GoIRAssignInst -> GoIRAssignInst(location, registers.getValue(source.register), expression(source.expr)) - is GoIRPhi -> GoIRPhi( - location, - registers.getValue(source.register), - source.edges.mapValues { value(it.value) }, - source.comment, - ) - is GoIRCall -> GoIRCall(location, registers.getValue(source.register), call(source.call)) - is GoIRJump -> source.copy(location = location) - is GoIRIf -> source.copy(location = location, cond = value(source.cond)) - is GoIRReturn -> source.copy(location = location, results = source.results.map(::value)) - is GoIRPanic -> source.copy(location = location, x = value(source.x)) - is GoIRFieldStore -> source.copy( - location = location, - addr = value(source.addr), - base = value(source.base), - value = value(source.value), - ) - is GoIRIndexStore -> source.copy( - location = location, - addr = value(source.addr), - base = value(source.base), - index = value(source.index), - value = value(source.value), - ) - is GoIRStore -> source.copy( - location = location, addr = value(source.addr), value = value(source.value), - ) - is GoIRGlobalStore -> source.copy(location = location, global = globals(source.global), value = value(source.value)) - is GoIRMapUpdate -> source.copy( - location = location, - map = value(source.map), - key = value(source.key), - value = value(source.value), - ) - is GoIRSend -> source.copy(location = location, chan = value(source.chan), x = value(source.x)) - is GoIRGo -> source.copy(location = location, call = call(source.call)) - is GoIRDefer -> source.copy(location = location, call = call(source.call)) - is GoIRRunDefers -> source.copy(location = location) - is GoIRDebugRef -> source.copy(location = location, x = value(source.x)) - } - } -} diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt index d6211dc843..fdebf59494 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt @@ -3,6 +3,9 @@ package org.opentaint.ir.go.client import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.IntOpenHashSet +import org.opentaint.ir.go.api.GoIRFunction +import org.opentaint.ir.go.api.GoIRGlobalReference +import org.opentaint.ir.go.api.reference import org.opentaint.ir.go.api.GoIRField import org.opentaint.ir.go.api.GoIRFreeVar import org.opentaint.ir.go.api.GoIRInterfaceMethod @@ -165,43 +168,34 @@ class GoIRDeserializer { val models: List, ) - internal data class DeserializationConfig( - val normalizer: ModelPathNormalizer? = null, - val ownedPackagePaths: Set = emptySet(), - ) { - fun path(value: String): String = normalizer?.path(value) ?: value - fun symbol(value: String): String = normalizer?.symbol(value) ?: value - fun owns(pkg: ProtoPackage): Boolean = pkg.importPath in ownedPackagePaths - } - internal fun deserializeProgram( serializedProgram: ProtoProgram, - config: DeserializationConfig = DeserializationConfig(), ): GoIRProgram { val resultPackages = hashMapOf() val anonymousInterfaces = Int2ObjectOpenHashMap() val result = GoIRProgramImpl(resultPackages, anonymousInterfaces) + programView = result - val pkgInfo = deserializePackages(serializedProgram.packagesList, config) + val pkgInfo = deserializePackages(serializedProgram.packagesList) serializedProgram.packagesList.forEach { serializedPackage -> val deserializedPackage = getPackage(serializedPackage.id) val importPath = deserializedPackage.importPath - if (config.owns(serializedPackage)) { - resultPackages[importPath] = deserializedPackage - } else { - resultPackages.putIfAbsent(importPath, deserializedPackage) - } + resultPackages.putIfAbsent(importPath, deserializedPackage) } - TypeDeserializationCtx(result, serializedProgram.typesList, pkgInfo, anonymousInterfaces, config) + TypeDeserializationCtx(result, serializedProgram.typesList, pkgInfo, anonymousInterfaces) .deserializeTypes() - FunctionDeserializationCtx(pkgInfo, result, config) + FunctionDeserializationCtx(pkgInfo, result) .deserializeFunctions() - serializedProgram.packagesList.forEach { deserializePackageMembers(it, config) } - serializedProgram.functionBodiesList.forEach { deserializeFunctionBody(it) } + serializedProgram.packagesList.forEach { deserializePackageMembers(it) } + serializedProgram.functionBodiesList.forEach { body -> + getFunction(body.functionId).setBodyReader { owner, context -> + deserializeFunctionBody(body, owner, context) + } + } pkgInfo.namedTypes.forEach { resolveNamedTypeBindings(it) } return result @@ -223,7 +217,6 @@ class GoIRDeserializer { private fun deserializePackages( packagesList: List, - config: DeserializationConfig, ): PackageInfo { val maxPackageId = packagesList.maxOfOrNull { it.id } ?: 0 val deserialized = arrayOfNulls(maxPackageId + 1).also { @@ -238,10 +231,10 @@ class GoIRDeserializer { for (pkg in packagesList) { deserialized[pkg.id] = GoIRPackageImpl( - importPath = config.path(pkg.importPath), + importPath = pkg.importPath, name = pkg.name, - isStdlib = pkg.isStdlib && !config.owns(pkg), - isDependency = pkg.isDependency || config.owns(pkg), + isStdlib = pkg.isStdlib, + isDependency = pkg.isDependency, ) pkg.namedTypesList.forEach { @@ -263,7 +256,6 @@ class GoIRDeserializer { typesList: List, val pkgInfo: PackageInfo, val anonymousInterfaces: Int2ObjectOpenHashMap, - val config: DeserializationConfig, ) { val typeDefinitions: Array = typesList.toArrayById { id } val namedTypeDefs: Array = pkgInfo.namedTypes.toArrayById { id } @@ -383,7 +375,7 @@ class GoIRDeserializer { val namedType = GoIRNamedTypeImpl( name = nt.name, - fullName = config.symbol(nt.fullName), + fullName = nt.fullName, pkg = pkg, underlying = resolveType(nt.underlyingTypeId), kind = namedTypeKindFromProto(nt.kind), @@ -556,7 +548,6 @@ class GoIRDeserializer { private class FunctionDeserializationCtx( val packageInfo: PackageInfo, val program: GoIRProgram, - val config: DeserializationConfig, ) private fun FunctionDeserializationCtx.deserializeFunctions() { @@ -582,7 +573,7 @@ class GoIRDeserializer { deserialized[pf.id] = GoIRFunctionImpl( name = pf.name, - fullName = config.symbol(pf.fullName), + fullName = pf.fullName, pkg = pkg, signature = getFunctionalType(pf.signatureTypeId), params = pf.paramsList.map { p -> GoIRParameter(p.name, getType(p.typeId), p.index) }, @@ -606,7 +597,6 @@ class GoIRDeserializer { originFullName = pf.originFunctionId.takeIf { it != 0 } ?.let { originId -> functions.getOrNull(originId)?.fullName - ?.let(config::symbol) }, typeArgs = pf.typeArgIdsList.map(::getType), ) @@ -633,7 +623,7 @@ class GoIRDeserializer { functions[id] ?: error("No function found for $id") - private fun deserializePackageMembers(pp: ProtoPackage, config: DeserializationConfig) { + private fun deserializePackageMembers(pp: ProtoPackage) { val pkg = getPackage(pp.id) for (pf in pp.functionsList) { @@ -645,7 +635,7 @@ class GoIRDeserializer { for (pg in pp.globalsList) { val global = GoIRGlobalImpl( name = pg.name, - fullName = config.symbol(pg.fullName), + fullName = pg.fullName, type = getType(pg.typeId), pkg = pkg, isExported = pg.isExported, @@ -659,7 +649,7 @@ class GoIRDeserializer { for (pc in pp.constantsList) { val const = GoIRConstImpl( name = pc.name, - fullName = config.symbol(pc.fullName), + fullName = pc.fullName, type = getType(pc.typeId), value = constValueFromProto(pc.value), pkg = pkg, @@ -682,8 +672,11 @@ class GoIRDeserializer { // ─── Function Bodies ──────────────────────────────────────────── - private fun deserializeFunctionBody(fb: ProtoFunctionBody) { - val fn = getFunction(fb.functionId) + private fun deserializeFunctionBody( + fb: ProtoFunctionBody, + fn: GoIRFunction, + context: GoIRBodyContext, + ): GoIRBodyImpl { val phiEdgeSyntheticLoads = collectPhiEdgeSyntheticLoads(fb, fn.fullName) val phiEdgeSyntheticLoadsByPred = phiEdgeSyntheticLoads.groupBy { it.predInstProtoIndex } @@ -725,7 +718,7 @@ class GoIRDeserializer { block.instructionsList.forEach { pi -> if (pi.valueId <= 0) return@forEach val newIdx = protoToNew.get(pi.index) - val reg = GoIRRegister(getType(pi.typeId), newIdx, pi.name) + val reg = GoIRRegister(context.type(getType(pi.typeId)), newIdx, pi.name) lazyValueMap.register(pi.valueId, reg) } } @@ -735,7 +728,7 @@ class GoIRDeserializer { for (load in phiEdgeSyntheticLoads) { val predBlockIndex = protoInstBlockIndices[load.predInstProtoIndex] ?: error("No predecessor block for phi edge pred inst ${load.predInstProtoIndex} in ${fn.fullName}") - val (reg, inst) = createSyntheticLoad(load.value, body, predBlockIndex, load.index) + val (reg, inst) = createSyntheticLoad(load.value, body, predBlockIndex, load.index, context) phiEdgeSyntheticValues[load.key] = reg phiEdgeSyntheticInstructions[load.key] = inst } @@ -743,7 +736,7 @@ class GoIRDeserializer { val ctx = InstContext(body, blockIdx = 0, fn = fn, valueMap = lazyValueMap, instructions = mutableListOf(), protoToNew = protoToNew, blockStartProtoToNew = blockStartProtoToNew, - phiEdgeSyntheticValues = phiEdgeSyntheticValues) + phiEdgeSyntheticValues = phiEdgeSyntheticValues, context = context) for ((blockIdx, pb) in fb.blocksList.withIndex()) { val block = blocks[blockIdx] ctx.blockIdx = blockIdx @@ -772,7 +765,7 @@ class GoIRDeserializer { } specializeStores(blocks) - fn.setBody(body) + return body } /** @@ -817,7 +810,7 @@ class GoIRDeserializer { pi: ProtoInstruction, ): GoIRInst { fun ref(vr: ProtoValueRef): GoIRValue = valueRefFromProto(vr, this) - fun type(id: Int): GoIRType = getType(id) + fun type(id: Int): GoIRType = context.type(getType(id)) fun translate(protoIdx: Int): GoIRInstRef = GoIRInstRef( protoToNew.get(protoIdx) ) @@ -865,7 +858,11 @@ class GoIRDeserializer { ) ProtoInstruction.InstCase.MAKE_CLOSURE -> { val closureFn = getFunction(pi.makeClosure.fnId) - assign(GoIRMakeClosureExpr(exprType(), closureFn, pi.makeClosure.bindingsList.map { ref(it) })) + assign(GoIRMakeClosureExpr( + exprType(), + context.function(functionReference(closureFn)), + pi.makeClosure.bindingsList.map { ref(it) }, + )) } ProtoInstruction.InstCase.MAKE_MAP -> assign( GoIRMakeMapExpr(exprType(), if (pi.makeMap.hasReserve) ref(pi.makeMap.reserve) else null) @@ -877,10 +874,18 @@ class GoIRDeserializer { GoIRMakeSliceExpr(exprType(), ref(pi.makeSlice.len), ref(pi.makeSlice.cap)) ) ProtoInstruction.InstCase.FIELD_ADDR -> assign( - GoIRFieldAddrExpr(exprType(), ref(pi.fieldAddr.x), pi.fieldAddr.fieldIndex, pi.fieldAddr.fieldName) + GoIRFieldAddrExpr( + exprType(), ref(pi.fieldAddr.x), + context.fieldIndex(type(pi.fieldAddr.x.typeId), pi.fieldAddr.fieldIndex, pi.fieldAddr.fieldName), + pi.fieldAddr.fieldName, + ) ) ProtoInstruction.InstCase.FIELD -> assign( - GoIRFieldExpr(exprType(), ref(pi.field.x), pi.field.fieldIndex, pi.field.fieldName) + GoIRFieldExpr( + exprType(), ref(pi.field.x), + context.fieldIndex(type(pi.field.x.typeId), pi.field.fieldIndex, pi.field.fieldName), + pi.field.fieldName, + ) ) ProtoInstruction.InstCase.INDEX_ADDR -> assign( GoIRIndexAddrExpr(exprType(), ref(pi.indexAddr.x), ref(pi.indexAddr.index)) @@ -980,7 +985,7 @@ class GoIRDeserializer { when (addr.refCase) { ProtoValueRef.RefCase.GLOBAL_ID -> GoIRGlobalStore( loc, - resolveGlobal(addr.globalId), + context.global(globalReference(addr.globalId)), ref(pi.store.`val`), ) ProtoValueRef.RefCase.FUNCTION_ID, @@ -1020,7 +1025,7 @@ class GoIRDeserializer { // ─── Value references ─────────────────────────────────────────── private fun valueRefFromProto(vr: ProtoValueRef, ctx: InstContext): GoIRValue { - val type = getType(vr.typeId) + val type = ctx.context.type(getType(vr.typeId)) return when (vr.refCase) { ProtoValueRef.RefCase.INST_VALUE_ID -> ctx.valueMap[vr.instValueId] @@ -1049,15 +1054,15 @@ class GoIRDeserializer { receiver = if (ci.hasReceiver()) valueRefFromProto(ci.receiver, ctx) else null, methodName = ci.methodName.ifEmpty { null }, args = ci.argsList.map { valueRefFromProto(it, ctx) }, - resultType = getType(ci.resultTypeId), + resultType = ctx.context.type(getType(ci.resultTypeId)), ) } private fun callTargetFromProto(vr: ProtoValueRef, ctx: InstContext): GoIRCallTarget { - val type = getType(vr.typeId) + val type = ctx.context.type(getType(vr.typeId)) return when (vr.refCase) { ProtoValueRef.RefCase.FUNCTION_ID -> - GoIRCallTarget.Function(getFunction(vr.functionId)) + GoIRCallTarget.Function(ctx.context.function(functionReference(getFunction(vr.functionId)))) ProtoValueRef.RefCase.BUILTIN_NAME -> GoIRCallTarget.Builtin(vr.builtinName, type) ProtoValueRef.RefCase.GLOBAL_ID -> @@ -1067,34 +1072,42 @@ class GoIRDeserializer { } } + private fun functionReference(function: GoIRFunction): GoIrFunctionReference = + function.reference().also { it.program = programView } + + private fun globalReference(id: Int): GoIRGlobalReference = + resolveGlobal(id).reference().also { it.program = programView } + + private lateinit var programView: GoIRProgram + private fun resolveGlobal(globalId: Int): GoIRGlobalImpl = globalsById.get(globalId) ?: error("Global was not found for $globalId") private fun synthLoadGlobal(globalId: Int, typeId: Int, ctx: InstContext): GoIRRegister { val idx = ctx.take() - val (reg, inst) = createSyntheticLoadGlobal(globalId, typeId, ctx.body, ctx.blockIdx, idx) + val (reg, inst) = createSyntheticLoadGlobal(globalId, typeId, ctx.body, ctx.blockIdx, idx, ctx.context) ctx.instructions.add(inst) return reg } private fun synthLoadFunction(functionId: Int, typeId: Int, ctx: InstContext): GoIRRegister { val idx = ctx.take() - val (reg, inst) = createSyntheticLoadFunction(functionId, typeId, ctx.body, ctx.blockIdx, idx) + val (reg, inst) = createSyntheticLoadFunction(functionId, typeId, ctx.body, ctx.blockIdx, idx, ctx.context) ctx.instructions.add(inst) return reg } private fun synthLoadBuiltin(name: String, typeId: Int, ctx: InstContext): GoIRRegister { val idx = ctx.take() - val (reg, inst) = createSyntheticLoadBuiltin(name, typeId, ctx.body, ctx.blockIdx, idx) + val (reg, inst) = createSyntheticLoadBuiltin(name, typeId, ctx.body, ctx.blockIdx, idx, ctx.context) ctx.instructions.add(inst) return reg } private fun synthLoadFreeVar(freeVarIndex: Int, typeId: Int, ctx: InstContext): GoIRRegister { val idx = ctx.take() - val (reg, inst) = createSyntheticLoadFreeVar(freeVarIndex, typeId, ctx.body, ctx.blockIdx, idx) + val (reg, inst) = createSyntheticLoadFreeVar(freeVarIndex, typeId, ctx.body, ctx.blockIdx, idx, ctx.context) ctx.instructions.add(inst) return reg } @@ -1104,11 +1117,12 @@ class GoIRDeserializer { body: GoIRBodyImpl, blockIndex: Int, index: Int, + context: GoIRBodyContext, ): Pair = when (vr.refCase) { - ProtoValueRef.RefCase.GLOBAL_ID -> createSyntheticLoadGlobal(vr.globalId, vr.typeId, body, blockIndex, index) - ProtoValueRef.RefCase.FUNCTION_ID -> createSyntheticLoadFunction(vr.functionId, vr.typeId, body, blockIndex, index) - ProtoValueRef.RefCase.BUILTIN_NAME -> createSyntheticLoadBuiltin(vr.builtinName, vr.typeId, body, blockIndex, index) - ProtoValueRef.RefCase.FREE_VAR_INDEX -> createSyntheticLoadFreeVar(vr.freeVarIndex, vr.typeId, body, blockIndex, index) + ProtoValueRef.RefCase.GLOBAL_ID -> createSyntheticLoadGlobal(vr.globalId, vr.typeId, body, blockIndex, index, context) + ProtoValueRef.RefCase.FUNCTION_ID -> createSyntheticLoadFunction(vr.functionId, vr.typeId, body, blockIndex, index, context) + ProtoValueRef.RefCase.BUILTIN_NAME -> createSyntheticLoadBuiltin(vr.builtinName, vr.typeId, body, blockIndex, index, context) + ProtoValueRef.RefCase.FREE_VAR_INDEX -> createSyntheticLoadFreeVar(vr.freeVarIndex, vr.typeId, body, blockIndex, index, context) else -> error("${vr.refCase} is not a synthetic-load ref") } @@ -1118,9 +1132,10 @@ class GoIRDeserializer { body: GoIRBodyImpl, blockIndex: Int, index: Int, + context: GoIRBodyContext, ): Pair { - val type = getType(typeId) - val global = resolveGlobal(globalId) + val type = context.type(getType(typeId)) + val global = context.global(globalReference(globalId)) val reg = GoIRRegister(type, index, "_g$index") val expr = GoIRGlobalValueExpr(type, global) val loc = GoInstLocation(body, index, blockIndex, null) @@ -1133,9 +1148,10 @@ class GoIRDeserializer { body: GoIRBodyImpl, blockIndex: Int, index: Int, + context: GoIRBodyContext, ): Pair { - val type = getType(typeId) - val func = getFunction(functionId) + val type = context.type(getType(typeId)) + val func = context.function(functionReference(getFunction(functionId))) val reg = GoIRRegister(type, index, "_f$index") val expr = GoIRFunctionValueExpr(type, func) val loc = GoInstLocation(body, index, blockIndex, null) @@ -1148,8 +1164,9 @@ class GoIRDeserializer { body: GoIRBodyImpl, blockIndex: Int, index: Int, + context: GoIRBodyContext, ): Pair { - val type = getType(typeId) + val type = context.type(getType(typeId)) val reg = GoIRRegister(type, index, "_b$index") val expr = GoIRBuiltinValueExpr(type, name) val loc = GoInstLocation(body, index, blockIndex, null) @@ -1162,8 +1179,9 @@ class GoIRDeserializer { body: GoIRBodyImpl, blockIndex: Int, index: Int, + context: GoIRBodyContext, ): Pair { - val type = getType(typeId) + val type = context.type(getType(typeId)) val freeVarName = body.function.freeVars[freeVarIndex].name val reg = GoIRRegister(type, index, "_v$index") val expr = GoIRFreeVarValueExpr(type, freeVarIndex, freeVarName) @@ -1418,12 +1436,13 @@ private data class PhiEdgeSyntheticLoad( private class InstContext( val body: GoIRBodyImpl, var blockIdx: Int, - val fn: GoIRFunctionImpl, + val fn: GoIRFunction, val valueMap: ValueMap, var instructions: MutableList, val protoToNew: Int2IntOpenHashMap, val blockStartProtoToNew: Int2IntOpenHashMap, val phiEdgeSyntheticValues: Map, + val context: GoIRBodyContext, ) { var nextIndex: Int = 0 fun take(): Int = nextIndex++ diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt index 934e52dfe3..e44d687a21 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt @@ -7,15 +7,13 @@ internal object GoIRModelLoader { fun apply(build: GoIRDeserializer.DeserializedGoIRBuild): GoIRProgram { if (build.models.isEmpty()) return build.program val definitions = build.models.map { model -> + val rawProgram = GoIRDeserializer().deserializeProgram(model.program) val normalizer = try { - ModelPathNormalizer.create(model.program) + ModelPathNormalizer.create(rawProgram) } catch (error: IllegalArgumentException) { throw IllegalArgumentException("invalid Go model ${model.source}: ${error.message}", error) } - val program = GoIRDeserializer().deserializeProgram( - model.program, - GoIRDeserializer.DeserializationConfig(normalizer, normalizer.replacements.keys), - ) + val program = GoIRTransformerChain(rawProgram, listOf(GoIRModelPathTransformer(normalizer))) GoIRModelDefinition(model.source, program, normalizer.targets) } return GoIRTransformerChain(build.program, listOf(GoIRModelTransformer(build.program, definitions))) diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt index 96813353b9..97beaa1297 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt @@ -12,11 +12,6 @@ import org.opentaint.ir.go.api.GoIRPackage import org.opentaint.ir.go.api.GoIRProgram import org.opentaint.ir.go.api.GoIRTransformer import org.opentaint.ir.go.api.GoIRTypeParamDecl -import org.opentaint.ir.go.expr.GoIRFieldAddrExpr -import org.opentaint.ir.go.expr.GoIRFieldExpr -import org.opentaint.ir.go.impl.GoIRBasicBlockImpl -import org.opentaint.ir.go.inst.GoIRAssignInst -import org.opentaint.ir.go.inst.GoIRFieldStore import org.opentaint.ir.go.type.GoIRAnonymousInterfaceType import org.opentaint.ir.go.type.GoIRAnonymousInterfaceTypeRef import org.opentaint.ir.go.type.GoIRArrayType @@ -48,8 +43,8 @@ internal class GoIRModelOverlayProgram( definitions: List, ) : GoIRProgram { private val definitionsByTarget = linkedMapOf() + val modeledPaths: Set get() = definitionsByTarget.keys val modelDependencies = linkedMapOf() - private val effectivePackages = mutableMapOf() init { definitions.forEach { definition -> @@ -85,11 +80,7 @@ internal class GoIRModelOverlayProgram( override fun findPackage(importPath: String): GoIRPackage? { val original = originalProgram.findPackage(importPath) ?: return modelDependencies[importPath] val definition = definitionsByTarget[importPath] ?: return original - return synchronized(effectivePackages) { - effectivePackages.getOrPut(importPath) { - createPackage(original, definition.program.findPackage(importPath)!!) - } - } + return createPackage(original, definition.program.findPackage(importPath)!!) } override fun allFunctions(): List = packages.values.flatMap { it.functions } @@ -139,73 +130,24 @@ internal class GoIRModelOverlayProgram( val replacement = modelByName[target.fullName] if (replacement != null && replacement.bodyAvailable && sameSignature(replacement, target)) { used += replacement - ApproximatedFunction(replacement, target) + ModelFunctionCandidate(replacement, target) } else { val genericModel = target.originFullName?.let(modelByName::get) if (genericModel != null && genericModel.bodyAvailable) { val specialized = specializer.specialize(genericModel, target) specializedAnonymousFunctions += specialized.anonymousFunctions - ApproximatedFunction(specialized.root, target) + ModelFunctionCandidate(specialized.root, target) } else { target } } }.toMutableList() - effectiveFunctions += specializedAnonymousFunctions.map(::ModelFunction) - effectiveFunctions += modelFunctions.filter { it !in used }.map(::ModelFunction) + effectiveFunctions += specializedAnonymousFunctions.map { ModelFunctionCandidate(it) } + effectiveFunctions += modelFunctions.filter { it !in used }.map { ModelFunctionCandidate(it) } - remapModelFieldAccesses(original, model) return GoIRModelOverlayPackage(original, model, effectiveFunctions) } - private fun remapModelFieldAccesses(original: GoIRPackage, model: GoIRPackage) { - val plans = model.namedTypes.mapNotNull { modelType -> - val targetType = original.findNamedType(modelType.name) ?: return@mapNotNull null - val targetByName = targetType.fields.associateBy { it.name } - val extraByName = modelType.fields - .filter { it.name !in targetByName } - .mapIndexed { index, field -> field.name to targetType.fields.size + index } - .toMap() - val indexes = modelType.fields.associate { field -> - field.index to (targetByName[field.name]?.index ?: extraByName.getValue(field.name)) - } - modelType.fullName to indexes - }.toMap() - - fun namedType(type: GoIRType): String? = when (type) { - is GoIRPointerType -> namedType(type.elem) - is GoIRNamedTypeRef -> type.typeRef.fullRefName - is GoIRStructType -> type.structTypeRef?.fullRefName - else -> null - } - fun index(type: GoIRType, oldIndex: Int): Int = - namedType(type)?.let(plans::get)?.get(oldIndex) ?: oldIndex - - model.functions.forEach { function -> - val body = function.body ?: return@forEach - body.blocks.forEach { block -> - val mutableBlock = block as? GoIRBasicBlockImpl ?: return@forEach - mutableBlock.setInstructions(block.instructions.map { instruction -> - when (instruction) { - is GoIRAssignInst -> when (val expression = instruction.expr) { - is GoIRFieldAddrExpr -> instruction.copy( - expr = expression.copy(fieldIndex = index(expression.x.type, expression.fieldIndex)), - ) - is GoIRFieldExpr -> instruction.copy( - expr = expression.copy(fieldIndex = index(expression.x.type, expression.fieldIndex)), - ) - else -> instruction - } - is GoIRFieldStore -> instruction.copy( - fieldIndex = index(instruction.base.type, instruction.fieldIndex), - ) - else -> instruction - } - }) - } - } - } - private fun validateFields(original: GoIRPackage, model: GoIRPackage, source: String) { model.namedTypes.forEach { modelType -> val targetType = original.findNamedType(modelType.name) ?: return@forEach @@ -432,8 +374,15 @@ private class GoIRModelOverlayNamedType( } } +private class ModelFunctionCandidate( + val implementation: GoIRFunction, + val original: GoIRFunction? = null, +) : GoIRFunction by implementation + private open class ModelFunction(protected val implementation: GoIRFunction) : GoIRModelFunction, - GoIRFunction by implementation { + GoIRFunction by implementation, GoIRBodyReader { + override fun readBody(owner: GoIRFunction, context: GoIRBodyContext) = + (implementation as GoIRBodyReader).readBody(owner, context) override val isSynthetic: Boolean get() = true } @@ -451,10 +400,18 @@ internal class GoIRModelTransformer( override val additionalPackages: List get() = models.modelDependencies.values.toList() - override fun transformPackage(pkg: GoIRPackage): GoIRPackage = - models.findPackage(pkg.importPath) ?: pkg + override fun resolveFunction( + reference: org.opentaint.ir.go.api.GoIrFunctionReference, + next: () -> GoIRFunction?, + ): GoIRFunction? { + val candidate = next() ?: return null + if (candidate !is ModelFunctionCandidate) return candidate + val original = candidate.original + return if (original == null) ModelFunction(candidate.implementation) + else ApproximatedFunction(candidate.implementation, original) + } + - override fun transformFunction(function: GoIRFunction): GoIRFunction = - function.pkg?.importPath?.let(models::findPackage)?.functions - ?.firstOrNull { it.fullName == function.fullName } ?: function + override fun resolvePackage(importPath: String, next: () -> GoIRPackage?): GoIRPackage? = + if (importPath in models.modeledPaths) models.findPackage(importPath) else next() } diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt index b49d6f8133..3317ea7ced 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt @@ -1,22 +1,13 @@ package org.opentaint.ir.go.client -import org.opentaint.ir.go.api.GoIRBody import org.opentaint.ir.go.api.GoIRFunction import org.opentaint.ir.go.api.GoIRParameter import org.opentaint.ir.go.api.GoIRFreeVar import org.opentaint.ir.go.api.GoIRProgram import org.opentaint.ir.go.api.GoIRTypeParamDecl import org.opentaint.ir.go.api.GoIrFunctionReference -import org.opentaint.ir.go.cfg.GoIRCallInfo -import org.opentaint.ir.go.cfg.GoIRCallTarget -import org.opentaint.ir.go.cfg.GoIRSelectState -import org.opentaint.ir.go.expr.* -import org.opentaint.ir.go.impl.GoIRBasicBlockImpl -import org.opentaint.ir.go.impl.GoIRBodyImpl import org.opentaint.ir.go.impl.GoIRFunctionImpl -import org.opentaint.ir.go.inst.* import org.opentaint.ir.go.type.* -import org.opentaint.ir.go.value.* import java.util.IdentityHashMap internal data class SpecializedGoModelFunction( @@ -24,7 +15,7 @@ internal data class SpecializedGoModelFunction( val anonymousFunctions: List, ) -/** Copies a generic model body and substitutes the target instance type arguments. */ +/** Substitutes concrete type arguments and closure lookups while reading a generic model body. */ internal class GoIRModelSpecializer( private val program: GoIRProgram, ) { @@ -111,7 +102,20 @@ internal class GoIRModelSpecializer( (listOf(modelRoot) + sourceFunctions).forEach { source -> val copied = functionMap.getValue(source) - copied.setBody(transformGoIRBody(source.body!!, copied, specializeType, { functionMap[it] ?: it })) + val reader = source as? GoIRBodyReader + ?: error("Go model ${source.fullName} has no body reader") + copied.setBodyReader { owner, context -> + reader.readBody(owner, object : GoIRBodyContext() { + override fun type(type: GoIRType) = context.type(specializeType(type)) + override fun function(ref: GoIrFunctionReference): GoIrFunctionReference { + val replacement = functionMap[ref.function] + return context.function(if (replacement == null) ref else reference(replacement)) + } + override fun global(ref: org.opentaint.ir.go.api.GoIRGlobalReference) = context.global(ref) + override fun fieldIndex(type: GoIRType, index: Int, name: String) = + context.fieldIndex(specializeType(type), index, name) + }) ?: error("Go model ${source.fullName} has no body") + } } return SpecializedGoModelFunction(root, sourceFunctions.map(functionMap::getValue)) } @@ -133,10 +137,7 @@ internal class GoIRModelSpecializer( private class TypeSpecializer( private val arguments: Map, ) : (GoIRType) -> GoIRType { - private val memo = IdentityHashMap() - override fun invoke(type: GoIRType): GoIRType { - memo[type]?.let { return it } val result = when (type) { is GoIRTypeParamType -> arguments[type.paramIndex] ?: type is GoIRTypeParamRef -> arguments[type.paramIndex] ?: type @@ -169,7 +170,6 @@ private class TypeSpecializer( is GoIRTupleType -> GoIRTupleType(type.elements.map(::invoke)) else -> type } - memo[type] = result return result } } diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt index dee6a6631f..a8bbf14b66 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt @@ -2,72 +2,75 @@ package org.opentaint.ir.go.client import org.opentaint.ir.go.api.* import org.opentaint.ir.go.type.* -import java.util.IdentityHashMap -/** A lazy, cached IR view. Every reference exposed by the view uses the same transformer chain. */ +/** Lookup view. Only finite package members and individual lookup results retain resolved values. */ class GoIRTransformerChain( private val source: GoIRProgram, private val transformers: List, ) : GoIRProgram { override val originalProgram: GoIRProgram get() = source.originalProgram - private val types = LookupTypes(this, source.anonymousInterfaces) - private val functionCache = IdentityHashMap() - private val globalCache = IdentityHashMap() - private val packageCache = mutableMapOf() + private val types = LookupTypes(this, source.anonymousInterfaces, ::path) + private fun path(value: String) = transformers.fold(value) { v, transformer -> transformer.packagePath(v) } + private fun symbol(value: String) = transformers.fold(value) { v, transformer -> transformer.symbolName(v) } private val additionalPackages by lazy { transformers.flatMap { it.additionalPackages }.associateBy { it.importPath } } + override val lookup: GoIRLookup = GoIRProgramLookup(this) override val packages: Map by lazy { - (source.packages.keys + additionalPackages.keys).associateWith { findPackage(it)!! } + (source.packages + additionalPackages).entries + .sortedBy { if (path(it.key) == it.key) 0 else 1 } + .associate { (key, original) -> + val resolvedPath = path(key) + resolvedPath to lazy { + fun resolve(index: Int): GoIRPackage? = if (index < 0) original else + transformers[index].resolvePackage(resolvedPath) { resolve(index - 1) } + PackageView(resolve(transformers.lastIndex) ?: original) + } + }.let { entries -> + object : AbstractMap() { + override fun get(key: String): GoIRPackage? = entries[key]?.value + override val entries: Set> + get() = entries.mapValues { it.value.value }.entries + } + } } override val anonymousInterfaces: Map get() = types.anonymousInterfaces - override fun findPackage(importPath: String): GoIRPackage? = synchronized(packageCache) { - packageCache[importPath]?.let { return it } - val original = source.findPackage(importPath) ?: additionalPackages[importPath] ?: return null - val transformed = transformers.fold(original) { pkg, transformer -> transformer.transformPackage(pkg) } - PackageView(transformed).also { packageCache[importPath] = it } - } + override fun findPackage(importPath: String): GoIRPackage? = packages[importPath] override fun allFunctions(): List = packages.values.flatMap { it.functions } override fun allNamedTypes(): List = packages.values.flatMap { it.namedTypes } override fun mainPackage(): GoIRPackage? = packages.values.find { it.name == "main" } - private fun function(source: GoIRFunction): GoIRFunction = synchronized(functionCache) { - functionCache[source]?.let { return it } - val transformed = transformers.fold(source) { fn, transformer -> transformer.transformFunction(fn) } - functionCache[transformed]?.let { functionCache[source] = it; return it } - val view = when (transformed) { - is GoIRApproximatedFunction -> ApproximationView(transformed) - is GoIRModelFunction -> ModelFunctionView(transformed) - else -> FunctionView(transformed) + private fun function(source: GoIRFunction): GoIRFunction { + val reference = reference(source.reference()) + fun resolve(index: Int): GoIRFunction? = if (index < 0) source else + transformers[index].resolveFunction(reference) { resolve(index - 1) } + return when (val resolved = resolve(transformers.lastIndex) ?: source) { + is GoIRApproximatedFunction -> ApproximationView(resolved) + is GoIRModelFunction -> ModelFunctionView(resolved) + else -> FunctionView(resolved, (resolved as? GoIRBodyReader) ?: (source as? GoIRBodyReader)) } - functionCache[source] = view - functionCache[transformed] = view - view } - private fun global(source: GoIRGlobal): GoIRGlobal = synchronized(globalCache) { - globalCache[source]?.let { return it } - val resolved = (findPackage(source.pkg.importPath) as? PackageView) - ?.source?.findGlobal(source.name) ?: source - globalCache[resolved]?.let { globalCache[source] = it; return it } - val view = object : GoIRGlobal by resolved { - override val pkg get() = findPackage(resolved.pkg.importPath)!! - override val type by lazy { types(resolved.type) } - } - globalCache[source] = view - globalCache[resolved] = view - view + private fun global(source: GoIRGlobal): GoIRGlobal = object : GoIRGlobal by source { + override val pkg get() = findPackage(path(source.pkg.importPath))!! + override val fullName get() = symbol(source.fullName) + override val type by lazy { types(source.type) } } private fun reference(ref: GoIrFunctionReference): GoIrFunctionReference = - ref.copy(signature = types(ref.signature) as GoIRFuncType).also { it.program = this } - - private open inner class FunctionView(protected val source: GoIRFunction) : GoIRFunction by source { - override val pkg: GoIRPackage? get() = source.pkg?.importPath?.let(::findPackage) + ref.copy(pkg = path(ref.pkg), signature = types(ref.signature) as GoIRFuncType).also { it.program = this } + + private open inner class FunctionView( + protected val source: GoIRFunction, + private val reader: GoIRBodyReader? = source as? GoIRBodyReader, + ) : GoIRFunction by source, GoIRBodyReader { + override val fullName get() = symbol(source.fullName) + override val originFullName get() = source.originFullName?.let(::symbol) + override val pkg: GoIRPackage? get() = source.pkg?.importPath?.let { findPackage(path(it)) } override val signature by lazy { types(source.signature) as GoIRFuncType } override val params by lazy { source.params.map { it.copy(type = types(it.type)) } } override val freeVars by lazy { source.freeVars.map { it.copy(type = types(it.type)) } } @@ -76,7 +79,28 @@ class GoIRTransformerChain( override val receiverType get() = signature.recv override val parent by lazy { source.parent?.let(::reference) } override val anonymousFunctions by lazy { source.anonymousFunctions.map(::reference) } - override val body by lazy { source.body?.let { transformGoIRBody(it, this, types, ::function, ::global) } } + override fun readBody(owner: GoIRFunction, context: GoIRBodyContext): GoIRBody? { + val mapped = object : GoIRBodyContext() { + override fun type(type: GoIRType) = context.type(types(type)) + override fun function(reference: GoIrFunctionReference) = context.function(reference(reference)) + override fun global(reference: GoIRGlobalReference) = context.global( + reference.copy(pkg = path(reference.pkg)).also { it.program = this@GoIRTransformerChain }, + ) + override fun fieldIndex(type: GoIRType, index: Int, name: String): Int { + val mappedType = types(type) + fun named(type: GoIRType): GoIRNamedType? = when (type) { + is GoIRPointerType -> named(type.elem) + is GoIRNamedTypeRef -> type.namedType + is GoIRStructType -> type.namedType + else -> null + } + val resolved = named(mappedType)?.fields?.firstOrNull { it.name == name }?.index ?: index + return context.fieldIndex(mappedType, resolved, name) + } + } + return reader?.readBody(owner, mapped) ?: source.body + } + override val body by lazy { readBody(this, GoIRBodyContext()) } override val hasBody get() = source.hasBody override val bodyAvailable get() = source.bodyAvailable override val parameters get() = if (freeVars.isEmpty()) params @@ -95,6 +119,7 @@ class GoIRTransformerChain( } private inner class PackageView(val source: GoIRPackage) : GoIRPackage by source { + override val importPath get() = path(source.importPath) override val functions by lazy { source.functions.map(::function) } override val namedTypes by lazy { source.namedTypes.map { NamedTypeView(it, this) } } override val globals by lazy { source.globals.map(::global) } @@ -102,30 +127,32 @@ class GoIRTransformerChain( source.constants.map { constant -> object : GoIRConst by constant { override val pkg get() = this@PackageView + override val fullName get() = symbol(constant.fullName) override val type by lazy { types(constant.type) } } } } override fun findGlobal(name: String) = globals.find { it.name == name } override fun findConstant(name: String) = constants.find { it.name == name } - override val imports by lazy { source.imports.map { findPackage(it.importPath) ?: it } } - override val initFunction get() = source.initFunction?.let(::function) + override val imports by lazy { source.imports.map { findPackage(path(it.importPath)) ?: it } } + override val initFunction by lazy { source.initFunction?.let { reference(it.reference()).function } } override fun findNamedType(name: String) = namedTypes.find { it.name == name } override fun allMethods() = namedTypes.flatMap { it.allMethods() } } private inner class NamedTypeView(private val source: GoIRNamedType, override val pkg: GoIRPackage) : GoIRNamedType by source { + override val fullName get() = symbol(source.fullName) override val underlying by lazy { types(source.underlying) } override val fields by lazy { source.fields.map { it.copy(type = types(it.type), enclosingType = this) } } - override val methods by lazy { source.methods.map(::function) } - override val pointerMethods by lazy { source.pointerMethods.map(::function) } + override val methods by lazy { source.methods.map { reference(it.reference()).function } } + override val pointerMethods by lazy { source.pointerMethods.map { reference(it.reference()).function } } override val typeParams by lazy { source.typeParams.map { it.copy(constraint = types(it.constraint)) } } override val interfaceMethods by lazy { source.interfaceMethods.map { it.copy(signature = types(it.signature) as GoIRFuncType, enclosingInterface = this) } } override val embeddedInterfaces by lazy { - source.embeddedInterfaces.map { findPackage(it.pkg.importPath)?.findNamedType(it.name) ?: it } + source.embeddedInterfaces.map { findPackage(path(it.pkg.importPath))?.findNamedType(it.name) ?: it } } override fun allMethods() = methods + pointerMethods override fun methodByName(name: String) = allMethods().find { it.name == name } @@ -135,11 +162,13 @@ class GoIRTransformerChain( private class LookupTypes( private val program: GoIRProgram, originalInterfaces: Map, + private val path: (String) -> String, ) : (GoIRType) -> GoIRType { - private val memo = IdentityHashMap() + private val lock = Any() - // Anonymous-interface IDs are local to each deserialized program. Intern structural - // definitions so model-only interfaces cannot collide with program interface IDs. + // This namespace registry holds interface definitions from the finite input programs. + // It does not memoize transformed type graphs. IDs are local to each wire program, + // so model definitions must be interned before entering the shared lookup namespace. private val interfaces = originalInterfaces.toMutableMap() private val interfaceIds = mutableMapOf().apply { originalInterfaces.forEach { (id, type) -> putIfAbsent(type, id) } @@ -148,11 +177,11 @@ private class LookupTypes( val anonymousInterfaces: Map = object : AbstractMap() { - override fun get(key: Int): GoIRAnonymousInterfaceType? = synchronized(memo) { + override fun get(key: Int): GoIRAnonymousInterfaceType? = synchronized(lock) { interfaces[key]?.let { invoke(it) as GoIRAnonymousInterfaceType } } override val entries: Set> - get() = synchronized(memo) { + get() = synchronized(lock) { interfaces.toMap().mapValues { invoke(it.value) as GoIRAnonymousInterfaceType }.entries } } @@ -161,10 +190,9 @@ private class LookupTypes( nextInterfaceId++.also { interfaces[it] = type } } - private fun reference(ref: NamedTypeRef) = ref.copy().also { it.program = program } + private fun reference(ref: NamedTypeRef) = ref.copy(refTypePkg = path(ref.refTypePkg)).also { it.program = program } - override fun invoke(type: GoIRType): GoIRType = synchronized(memo) { - memo[type]?.let { return it } + override fun invoke(type: GoIRType): GoIRType = synchronized(lock) { val result = when (type) { is GoIRTypeParamType -> type.copy(constraint = invoke(type.constraint)) is GoIRTypeParamRef -> type @@ -199,7 +227,6 @@ private class LookupTypes( is GoIRTupleType -> GoIRTupleType(type.elements.map(::invoke)) else -> type } - memo[type] = result result } } diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/ModelPathNormalizer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/ModelPathNormalizer.kt index 03621bfc84..d2608f886d 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/ModelPathNormalizer.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/ModelPathNormalizer.kt @@ -1,6 +1,6 @@ package org.opentaint.ir.go.client -import org.opentaint.ir.go.proto.ProtoProgram +import org.opentaint.ir.go.api.* internal data class ModelPathNormalizer( val replacements: Map, @@ -14,8 +14,8 @@ internal data class ModelPathNormalizer( } companion object { - fun create(program: ProtoProgram): ModelPathNormalizer { - val replacements = program.packagesList + fun create(program: GoIRProgram): ModelPathNormalizer { + val replacements = program.packages.values .filterNot { it.isDependency || it.isStdlib || it.importPath == UNIVERSE_PACKAGE_PATH } .associate { pkg -> require(pkg.importPath.startsWith(GO_MODEL_PREFIX)) { @@ -43,3 +43,16 @@ internal data class ModelPathNormalizer( private const val GO_MODEL_PREFIX = "opentaint/" private const val UNIVERSE_PACKAGE_PATH = "" + +internal class GoIRModelPathTransformer(private val normalizer: ModelPathNormalizer) : GoIRTransformer { + override fun packagePath(importPath: String) = normalizer.path(importPath) + override fun symbolName(fullName: String) = normalizer.symbol(fullName) + override fun resolvePackage(importPath: String, next: () -> GoIRPackage?): GoIRPackage? { + val pkg = next() ?: return null + if (pkg.importPath !in normalizer.replacements) return pkg + return object : GoIRPackage by pkg { + override val isStdlib = false + override val isDependency = true + } + } +} diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt index b24d2d61d3..d8e1f15256 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt @@ -1,5 +1,7 @@ package org.opentaint.ir.go.impl +import org.opentaint.ir.go.client.GoIRBodyReader +import org.opentaint.ir.go.client.GoIRBodyContext import org.opentaint.ir.go.api.GoIRBody import org.opentaint.ir.go.api.GoIRBodyUnavailableException import org.opentaint.ir.go.api.GoIRFreeVar @@ -15,7 +17,7 @@ import org.opentaint.ir.go.type.GoIRType class GoIRFunctionImpl( override val name: String, override val fullName: String, - pkg: GoIRPackage?, + override val pkg: GoIRPackage?, override val signature: GoIRFuncType, override val params: List, override val freeVars: List, @@ -31,26 +33,31 @@ class GoIRFunctionImpl( override val typeParams: List = emptyList(), override val originFullName: String? = null, override val typeArgs: List = emptyList(), -) : GoIRFunction { - private var owner = pkg - override val pkg: GoIRPackage? get() = owner +) : GoIRFunction, GoIRBodyReader { - internal fun rebindPackage(pkg: GoIRPackage) { - owner = pkg + private var _body: GoIRBody? = null + private var reader: ((GoIRFunction, GoIRBodyContext) -> GoIRBody)? = null + + internal fun setBodyReader(reader: (GoIRFunction, GoIRBodyContext) -> GoIRBody) { + this.reader = reader } - private var _body: GoIRBody? = null + override fun readBody(owner: GoIRFunction, context: GoIRBodyContext): GoIRBody? = + reader?.invoke(owner, context) ?: _body + + private val resolvedBody by lazy { reader?.invoke(this, GoIRBodyContext()) } override val body: GoIRBody? get() { val b = _body if (b != null) return b + resolvedBody?.let { return it } if (!declaredHasBody) return null throw GoIRBodyUnavailableException(fullName) } override val hasBody: Boolean get() = declaredHasBody - override val bodyAvailable: Boolean get() = _body != null + override val bodyAvailable: Boolean get() = _body != null || reader != null fun setBody(body: GoIRBody) { this._body = body diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRGlobalImpl.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRGlobalImpl.kt index 7b31f8c6a1..c670986ec3 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRGlobalImpl.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRGlobalImpl.kt @@ -8,16 +8,10 @@ class GoIRGlobalImpl( override val name: String, override val fullName: String, override val type: GoIRType, - pkg: GoIRPackage, + override val pkg: GoIRPackage, override val isExported: Boolean, override val position: GoIRPosition?, ) : GoIRGlobal { - private var owner = pkg - override val pkg: GoIRPackage get() = owner - - internal fun rebindPackage(pkg: GoIRPackage) { - owner = pkg - } override fun equals(other: Any?): Boolean { if (this === other) return true @@ -33,16 +27,10 @@ class GoIRConstImpl( override val fullName: String, override val type: GoIRType, override val value: GoIRConstantValue, - pkg: GoIRPackage, + override val pkg: GoIRPackage, override val isExported: Boolean, override val position: GoIRPosition?, ) : GoIRConst { - private var owner = pkg - override val pkg: GoIRPackage get() = owner - - internal fun rebindPackage(pkg: GoIRPackage) { - owner = pkg - } override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRNamedTypeImpl.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRNamedTypeImpl.kt index 85dcb93626..34ce609648 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRNamedTypeImpl.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRNamedTypeImpl.kt @@ -13,18 +13,12 @@ import org.opentaint.ir.go.type.GoIRType class GoIRNamedTypeImpl( override val name: String, override val fullName: String, - pkg: GoIRPackage, + override val pkg: GoIRPackage, override val underlying: GoIRType, override val kind: GoIRNamedTypeKind, override val position: GoIRPosition?, override val typeParams: List = emptyList(), ) : GoIRNamedType { - private var owner = pkg - override val pkg: GoIRPackage get() = owner - - internal fun rebindPackage(pkg: GoIRPackage) { - owner = pkg - } override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt index 6c2efc0c4c..41e320d6bb 100644 --- a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt +++ b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt @@ -7,6 +7,7 @@ import org.junit.jupiter.api.extension.ExtendWith import org.opentaint.ir.go.api.GoIRApproximatedFunction import org.opentaint.ir.go.api.GoIRModelFunction import org.opentaint.ir.go.api.GoIRFunction +import org.opentaint.ir.go.api.reference import org.opentaint.ir.go.api.GoIRTransformer import org.opentaint.ir.go.client.GoIRTransformerChain import org.opentaint.ir.go.cfg.GoIRCallTarget @@ -757,10 +758,15 @@ class ModelTests { fun `transformers compose in order and resolve calls through one cached view`(builder: GoIRTestBuilder) { val original = builder.buildFromSource("package p\nfunc Target() {}\nfunc Caller() { Target() }\n") fun suffix(value: String) = object : GoIRTransformer { - override fun transformFunction(function: GoIRFunction): GoIRFunction = - object : GoIRFunction by function { + override fun resolveFunction( + reference: org.opentaint.ir.go.api.GoIrFunctionReference, + next: () -> GoIRFunction?, + ): GoIRFunction? { + val function = next() ?: return null + return object : GoIRFunction by function { override val syntheticKind = function.syntheticKind.orEmpty() + value } + } } val transformed = GoIRTransformerChain(original, listOf(suffix("first"), suffix("second"))) val pkg = transformed.packages.getValue("test/p") @@ -773,6 +779,60 @@ class ModelTests { assertThat(original.findPackage("test/p")!!.functions.single { it.name == "Target" }.syntheticKind).isNull() } + @Test + fun `lookup interceptor can bypass later resolvers and leaves bodies unread`(builder: GoIRTestBuilder) { + val original = builder.buildFromSource("package p\nfunc Target() {}\nfunc Caller() { Target() }\n") + val target = original.findPackage("test/p")!!.functions.single { it.name == "Target" } + var ownedFallbacks = 0 + var bodyReads = 0 + val fallback = object : GoIRTransformer { + override fun resolveFunction( + reference: org.opentaint.ir.go.api.GoIrFunctionReference, + next: () -> GoIRFunction?, + ): GoIRFunction? { + if (reference.name == "Target") ownedFallbacks++ + return next() + } + } + val interceptor = object : GoIRTransformer { + override fun resolveFunction( + reference: org.opentaint.ir.go.api.GoIrFunctionReference, + next: () -> GoIRFunction?, + ): GoIRFunction? { + if (reference.name != "Target") return next() + return object : GoIRFunction by target { + override val syntheticKind = "intercepted" + override val body get() = target.body.also { bodyReads++ } + } + } + } + val program = GoIRTransformerChain(original, listOf(fallback, interceptor)) + val pkg = program.findPackage("test/p")!! + val resolved = pkg.functions.single { it.name == "Target" } + val call = pkg.functions.single { it.name == "Caller" } + .findInstructions().single().call.target as GoIRCallTarget.Function + assertThat(resolved.syntheticKind).isEqualTo("intercepted") + assertThat(call.lookup.pkg).isEqualTo("test/p") + assertThat(call.function).isSameAs(resolved) + assertThat(call.function).isSameAs(resolved) + assertThat(ownedFallbacks).isZero() + assertThat(bodyReads).isZero() + } + + @Test + fun `method and initializer lookup reuse package members`(builder: GoIRTestBuilder) { + val original = builder.buildFromSource( + "package p\ntype Box struct {}\nfunc (b *Box) Method() {}\nfunc init() {}\n", + ) + val program = GoIRTransformerChain(original, emptyList()) + val pkg = program.findPackage("test/p")!! + val method = pkg.functions.single { it.name == "Method" } + assertThat(pkg.findNamedType("Box")!!.pointerMethods.single()).isSameAs(method) + val call = GoIRCallTarget.Function(method.reference().also { it.program = program }) + assertThat(call.displayName).isEqualTo(method.fullName) + assertThat(pkg.initFunction).isSameAs(pkg.functions.single { it.name == "init" }) + } + private fun project(module: String, source: String): Path { val project = Files.createTempDirectory("goir-model-project") project.resolve("go.mod").write("module $module\ngo 1.22\n") From 4b473b486a5f312509290ddde6ddeb170269bbcd Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Tue, 27 Oct 2026 13:14:12 +0100 Subject: [PATCH 10/12] fix(go-models): ignore models for absent target packages --- .../ir/go/client/GoIRModelOverlay.kt | 3 ++ .../ir/go/test/features/ModelTests.kt | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt index 97beaa1297..e21e413878 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt @@ -56,10 +56,13 @@ internal class GoIRModelOverlayProgram( } } } + // Bundled modules can describe dependencies that this project does not use. + definitionsByTarget.entries.removeIf { (target, _) -> originalProgram.findPackage(target) == null } definitionsByTarget.forEach { (target, definition) -> validatePackage(target, definition) } definitions.forEach { definition -> + if (definition.targetPaths.none { it in definitionsByTarget }) return@forEach definition.program.packages.forEach { (path, pkg) -> if (path !in definition.targetPaths && originalProgram.findPackage(path) == null) { modelDependencies.putIfAbsent(path, pkg) diff --git a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt index 41e320d6bb..7d5fddbe3b 100644 --- a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt +++ b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt @@ -583,6 +583,45 @@ class ModelTests { assertThat((callTarget as GoIRCallTarget.Function).function).isSameAs(helper) } + @Test + fun `unrelated model does not add packages to the analyzed project`(builder: GoIRTestBuilder) { + val project = project("example.com/unrelated", "package unrelated\nfunc Target() {}\n") + val model = model( + "example.com/absent", + "package absent\nimport \"net/url\"\n" + + "func Identity(value string) string { return url.QueryEscape(value) }\n", + ) + + val program = builder.buildFromDir(project, modelConfig(model)) + + assertThat(program.findPackage("example.com/unrelated")).isNotNull() + assertThat(program.findPackage("example.com/absent")).isNull() + assertThat(program.findPackage("net/url")).isNull() + assertThat(program.packages.keys).containsExactlyInAnyOrderElementsOf(program.originalProgram.packages.keys) + } + + @Test + fun `model module applies only to packages present in the project`(builder: GoIRTestBuilder) { + val project = project( + "example.com/partial", + "package partial\nfunc Identity(value string) string { return \"original\" }\n", + ) + val model = model( + "example.com/partial", + "package partial\nfunc Identity(value string) string { return value }\n", + ) + val absent = model.resolve("example.com/absent") + Files.createDirectories(absent) + absent.resolve("model.go").write("package absent\nfunc Target() {}\n") + + val program = builder.buildFromDir(project, modelConfig(model)) + val identity = program.findPackage("example.com/partial")!!.functions.single { it.name == "Identity" } + val returned = (identity.body!!.blocks.single().terminator as GoIRReturn).results.single() + + assertThat(returned).isInstanceOf(GoIRParameterValue::class.java) + assertThat(program.findPackage("example.com/absent")).isNull() + } + @Test fun `model-only dependency is visible only through effective lookup`(builder: GoIRTestBuilder) { val project = project( From 3050bf1e2eacc58fa35f87596534bbca52840357 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Tue, 27 Oct 2026 21:52:17 +0100 Subject: [PATCH 11/12] refactor(go-models): collapse model views and fix their identity Address review 5673574297 by removing the intermediate model entities and giving the remaining views an equals/hashCode consistent with the IR implementations. - The model transformer is no longer a GoIRProgram; it intercepts package lookup and builds GoIRPackageImpl/GoIRNamedTypeImpl instead of the overlay package and named-type views. - ModelFunctionCandidate and the transformer's function re-wrapping are gone; the overlay emits ModelFunction/ApproximatedFunction directly. - ModelPathNormalizer is folded into GoIRModelPathTransformer, dropping its unused package view. - The transformer chain builds GoIRGlobalImpl/GoIRConstImpl instead of anonymous delegating views. - Package and named-type views compare by import path/full name, like their Impl counterparts. - Function identity is fullName plus model-ness, shared by GoIRFunctionImpl and all function views, so an approximation never equals the function it replaces (bodies and instruction locations inherit function equality). Validation: 134 Go IR tests (new identity regression included), GoUnitResolverTest and EntryPointSelectionTest pass; git diff --check clean. --- .../opentaint/ir/go/client/GoIRModelLoader.kt | 7 +- .../ir/go/client/GoIRModelOverlay.kt | 269 ++++++------------ ...malizer.kt => GoIRModelPathTransformer.kt} | 37 +-- .../ir/go/client/GoIRModelSpecializer.kt | 10 +- .../ir/go/client/GoIRTransformerChain.kt | 36 +-- .../ir/go/impl/GoIRFunctionIdentity.kt | 18 ++ .../opentaint/ir/go/impl/GoIRFunctionImpl.kt | 8 +- .../ir/go/test/features/ModelTests.kt | 46 +++ 8 files changed, 187 insertions(+), 244 deletions(-) rename core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/{ModelPathNormalizer.kt => GoIRModelPathTransformer.kt} (51%) create mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionIdentity.kt diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt index e44d687a21..e7dd45be84 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt @@ -8,13 +8,12 @@ internal object GoIRModelLoader { if (build.models.isEmpty()) return build.program val definitions = build.models.map { model -> val rawProgram = GoIRDeserializer().deserializeProgram(model.program) - val normalizer = try { - ModelPathNormalizer.create(rawProgram) + val paths = try { + GoIRModelPathTransformer.create(rawProgram) } catch (error: IllegalArgumentException) { throw IllegalArgumentException("invalid Go model ${model.source}: ${error.message}", error) } - val program = GoIRTransformerChain(rawProgram, listOf(GoIRModelPathTransformer(normalizer))) - GoIRModelDefinition(model.source, program, normalizer.targets) + GoIRModelDefinition(model.source, GoIRTransformerChain(rawProgram, listOf(paths)), paths.targets) } return GoIRTransformerChain(build.program, listOf(GoIRModelTransformer(build.program, definitions))) } diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt index e21e413878..37d1fdcc45 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt @@ -1,17 +1,17 @@ package org.opentaint.ir.go.client import org.opentaint.ir.go.api.GoIRApproximatedFunction -import org.opentaint.ir.go.api.GoIRConst -import org.opentaint.ir.go.api.GoIRField +import org.opentaint.ir.go.api.GoIRBody import org.opentaint.ir.go.api.GoIRFunction -import org.opentaint.ir.go.api.GoIRGlobal -import org.opentaint.ir.go.api.GoIRInterfaceMethod import org.opentaint.ir.go.api.GoIRModelFunction import org.opentaint.ir.go.api.GoIRNamedType import org.opentaint.ir.go.api.GoIRPackage import org.opentaint.ir.go.api.GoIRProgram import org.opentaint.ir.go.api.GoIRTransformer -import org.opentaint.ir.go.api.GoIRTypeParamDecl +import org.opentaint.ir.go.impl.GoIRNamedTypeImpl +import org.opentaint.ir.go.impl.GoIRPackageImpl +import org.opentaint.ir.go.impl.functionEquals +import org.opentaint.ir.go.impl.functionHashCode import org.opentaint.ir.go.type.GoIRAnonymousInterfaceType import org.opentaint.ir.go.type.GoIRAnonymousInterfaceTypeRef import org.opentaint.ir.go.type.GoIRArrayType @@ -20,7 +20,6 @@ import org.opentaint.ir.go.type.GoIRChanType import org.opentaint.ir.go.type.GoIRFuncType import org.opentaint.ir.go.type.GoIRMapType import org.opentaint.ir.go.type.GoIRNamedInterfaceType -import org.opentaint.ir.go.type.GoIRNamedTypeKind import org.opentaint.ir.go.type.GoIRNamedTypeRef import org.opentaint.ir.go.type.GoIRPointerType import org.opentaint.ir.go.type.GoIRSliceType @@ -37,14 +36,13 @@ internal data class GoIRModelDefinition( val targetPaths: Set, ) -/** A read-only view that applies partial Go models when a package is looked up. */ -internal class GoIRModelOverlayProgram( - override val originalProgram: GoIRProgram, +/** Applies partial Go models by intercepting the lookup of every modeled package. */ +internal class GoIRModelTransformer( + private val originalProgram: GoIRProgram, definitions: List, -) : GoIRProgram { +) : GoIRTransformer { private val definitionsByTarget = linkedMapOf() - val modeledPaths: Set get() = definitionsByTarget.keys - val modelDependencies = linkedMapOf() + private val modelDependencies = linkedMapOf() init { definitions.forEach { definition -> @@ -71,27 +69,14 @@ internal class GoIRModelOverlayProgram( } } - override val packages: Map by lazy { - (originalProgram.packages.keys + modelDependencies.keys).associateWithTo(linkedMapOf()) { path -> - findPackage(path) ?: error("Go package $path disappeared during model lookup") - } - } - - override val anonymousInterfaces: Map - get() = originalProgram.anonymousInterfaces + override val additionalPackages: List get() = modelDependencies.values.toList() - override fun findPackage(importPath: String): GoIRPackage? { - val original = originalProgram.findPackage(importPath) ?: return modelDependencies[importPath] - val definition = definitionsByTarget[importPath] ?: return original + override fun resolvePackage(importPath: String, next: () -> GoIRPackage?): GoIRPackage? { + val definition = definitionsByTarget[importPath] ?: return next() + val original = next() ?: return null return createPackage(original, definition.program.findPackage(importPath)!!) } - override fun allFunctions(): List = packages.values.flatMap { it.functions } - - override fun allNamedTypes(): List = packages.values.flatMap { it.namedTypes } - - override fun mainPackage(): GoIRPackage? = packages.values.find { it.name == "main" } - private fun validatePackage(target: String, definition: GoIRModelDefinition) { val original = originalProgram.findPackage(target) ?: throw IllegalArgumentException( @@ -128,27 +113,77 @@ internal class GoIRModelOverlayProgram( .associateBy { it.fullName } val used = mutableSetOf() val specializedAnonymousFunctions = mutableListOf() - val specializer = GoIRModelSpecializer(this) - val effectiveFunctions = original.functions.map { target -> + val specializer = GoIRModelSpecializer(originalProgram) + val result = GoIRPackageImpl(original.importPath, original.name, original.isStdlib, original.isDependency) + original.functions.forEach { target -> val replacement = modelByName[target.fullName] - if (replacement != null && replacement.bodyAvailable && sameSignature(replacement, target)) { - used += replacement - ModelFunctionCandidate(replacement, target) - } else { - val genericModel = target.originFullName?.let(modelByName::get) - if (genericModel != null && genericModel.bodyAvailable) { + val genericModel = target.originFullName?.let(modelByName::get) + result.addFunction( + if (replacement != null && replacement.bodyAvailable && sameSignature(replacement, target)) { + used += replacement + ApproximatedFunction(replacement, target) + } else if (genericModel != null && genericModel.bodyAvailable) { val specialized = specializer.specialize(genericModel, target) - specializedAnonymousFunctions += specialized.anonymousFunctions - ModelFunctionCandidate(specialized.root, target) + specializedAnonymousFunctions += specialized.drop(1) + ApproximatedFunction(specialized.first(), target) } else { target - } - } - }.toMutableList() - effectiveFunctions += specializedAnonymousFunctions.map { ModelFunctionCandidate(it) } - effectiveFunctions += modelFunctions.filter { it !in used }.map { ModelFunctionCandidate(it) } + }, + ) + } + specializedAnonymousFunctions.forEach { result.addFunction(ModelFunction(it)) } + modelFunctions.filter { it !in used }.forEach { result.addFunction(ModelFunction(it)) } + + val functionsByName = buildMap { result.functions.forEach { putIfAbsent(it.fullName, it) } } + val modelTypes = model.namedTypes.associateBy { it.name } + original.namedTypes.forEach { target -> + val modelType = modelTypes[target.name] + result.addNamedType(modelType?.let { mergeNamedType(target, it, result, functionsByName) } ?: target) + } + model.namedTypes.filter { original.findNamedType(it.name) == null }.forEach(result::addNamedType) + (original.globals + model.globals).forEach { if (result.findGlobal(it.name) == null) result.addGlobal(it) } + (original.constants + model.constants).forEach { if (result.findConstant(it.name) == null) result.addConst(it) } + (original.imports + model.imports).distinctBy { it.importPath }.forEach { result._imports += it } + result.initFunction = original.initFunction + return result + } + + private fun mergeNamedType( + original: GoIRNamedType, + model: GoIRNamedType, + pkg: GoIRPackage, + functionsByName: Map, + ): GoIRNamedType { + val originalNames = original.fields.mapTo(mutableSetOf()) { it.name } + val fields = original.fields + model.fields.filter { it.name !in originalNames } + .mapIndexed { index, field -> field.copy(index = original.fields.size + index) } + val originalStruct = original.underlying as? GoIRStructType + val underlying = originalStruct?.let { struct -> + GoIRStructType( + fields.map { GoIRStructField(it.name, it.type, it.isEmbedded, it.tag) }, + struct.structTypeRef, + ) + } ?: original.underlying + val result = GoIRNamedTypeImpl( + original.name, original.fullName, pkg, underlying, original.kind, original.position, original.typeParams, + ) + fields.mapTo(result._fields) { it.copy(enclosingType = result) } + effectiveMethods(original.methods, model.methods, functionsByName).let(result._methods::addAll) + effectiveMethods(original.pointerMethods, model.pointerMethods, functionsByName) + .let(result._pointerMethods::addAll) + result._interfaceMethods += original.interfaceMethods + result._embeddedInterfaces += original.embeddedInterfaces + return result + } - return GoIRModelOverlayPackage(original, model, effectiveFunctions) + private fun effectiveMethods( + targetMethods: List, + modelMethods: List, + functionsByName: Map, + ): List { + val targetNames = targetMethods.mapTo(mutableSetOf()) { it.fullName } + return (targetMethods + modelMethods.filter { it.fullName !in targetNames }) + .map { functionsByName[it.fullName] ?: it } } private fun validateFields(original: GoIRPackage, model: GoIRPackage, source: String) { @@ -267,154 +302,18 @@ internal class GoIRModelOverlayProgram( } } -private class GoIRModelOverlayPackage( - private val original: GoIRPackage, - private val model: GoIRPackage, - override val functions: List, -) : GoIRPackage { - override val importPath: String get() = original.importPath - override val name: String get() = original.name - override val isStdlib: Boolean get() = original.isStdlib - override val isDependency: Boolean get() = original.isDependency - - private val functionsByName = functions.groupBy { it.fullName } - - override val namedTypes: List by lazy { - val modelByName = model.namedTypes.associateBy { it.name } - val result = original.namedTypes.map { target -> - val modelType = modelByName[target.name] - if (modelType == null) target else GoIRModelOverlayNamedType(target, modelType, this, functionsByName) - }.toMutableList() - val originalNames = original.namedTypes.mapTo(mutableSetOf()) { it.name } - result += model.namedTypes.filter { it.name !in originalNames } - result - } - - override val globals: List by lazy { - appendByName(original.globals, model.globals) { it.name } - } - override val constants: List by lazy { - appendByName(original.constants, model.constants) { it.name } - } - override val imports: List by lazy { - appendByName(original.imports, model.imports) { it.importPath } - } - override val initFunction: GoIRFunction? get() = original.initFunction - - override fun findNamedType(name: String): GoIRNamedType? = namedTypes.find { it.name == name } - override fun findGlobal(name: String): GoIRGlobal? = globals.find { it.name == name } - override fun findConstant(name: String): GoIRConst? = constants.find { it.name == name } - override fun allMethods(): List = namedTypes.flatMap { it.allMethods() } - - private fun appendByName(first: List, second: List, name: (T) -> String): List { - val names = first.mapTo(mutableSetOf(), name) - return first + second.filter { names.add(name(it)) } - } -} - -private class GoIRModelOverlayNamedType( - private val original: GoIRNamedType, - private val model: GoIRNamedType, - override val pkg: GoIRPackage, - private val functionsByName: Map>, -) : GoIRNamedType { - override val name: String get() = original.name - override val fullName: String get() = original.fullName - override val underlying: GoIRType by lazy { - val originalStruct = original.underlying as? GoIRStructType - if (originalStruct == null) { - original.underlying - } else { - GoIRStructType( - fields.map { field -> - GoIRStructField( - field.name, - field.type, - field.isEmbedded, - field.tag, - ) - }, - originalStruct.structTypeRef, - ) - } - } - override val kind: GoIRNamedTypeKind get() = original.kind - override val position get() = original.position - override val typeParams: List get() = original.typeParams - - override val fields: List by lazy { - val targetByName = original.fields.associateBy { it.name } - val result = original.fields.map { field -> field.copy(enclosingType = this) }.toMutableList() - model.fields.filter { it.name !in targetByName }.forEach { field -> - result += field.copy(index = result.size, enclosingType = this) - } - result - } - - override val methods: List by lazy { effectiveMethods(original.methods, model.methods) } - override val pointerMethods: List by lazy { - effectiveMethods(original.pointerMethods, model.pointerMethods) - } - override val interfaceMethods: List get() = original.interfaceMethods - override val embeddedInterfaces: List get() = original.embeddedInterfaces - - private fun effectiveMethods( - targetMethods: List, - modelMethods: List, - ): List { - val targetNames = targetMethods.mapTo(mutableSetOf()) { it.fullName } - val result = targetMethods.map { target -> - functionsByName[target.fullName] - ?.firstOrNull() - ?: target - }.toMutableList() - modelMethods.filter { it.fullName !in targetNames }.forEach { modelMethod -> - result += functionsByName[modelMethod.fullName] - ?.firstOrNull() - ?: modelMethod - } - return result - } -} - -private class ModelFunctionCandidate( - val implementation: GoIRFunction, - val original: GoIRFunction? = null, -) : GoIRFunction by implementation private open class ModelFunction(protected val implementation: GoIRFunction) : GoIRModelFunction, GoIRFunction by implementation, GoIRBodyReader { - override fun readBody(owner: GoIRFunction, context: GoIRBodyContext) = + override fun readBody(owner: GoIRFunction, context: GoIRBodyContext): GoIRBody? = (implementation as GoIRBodyReader).readBody(owner, context) override val isSynthetic: Boolean get() = true + override fun equals(other: Any?): Boolean = functionEquals(other) + override fun hashCode(): Int = functionHashCode() + override fun toString(): String = "GoIRModelFunction($fullName)" } private class ApproximatedFunction( implementation: GoIRFunction, override val original: GoIRFunction, ) : ModelFunction(implementation), GoIRApproximatedFunction - -/** Resolves models as a lookup transformer; deserialization never applies it. */ -internal class GoIRModelTransformer( - program: GoIRProgram, - definitions: List, -) : GoIRTransformer { - private val models = GoIRModelOverlayProgram(program, definitions) - - override val additionalPackages: List get() = models.modelDependencies.values.toList() - - override fun resolveFunction( - reference: org.opentaint.ir.go.api.GoIrFunctionReference, - next: () -> GoIRFunction?, - ): GoIRFunction? { - val candidate = next() ?: return null - if (candidate !is ModelFunctionCandidate) return candidate - val original = candidate.original - return if (original == null) ModelFunction(candidate.implementation) - else ApproximatedFunction(candidate.implementation, original) - } - - - override fun resolvePackage(importPath: String, next: () -> GoIRPackage?): GoIRPackage? = - if (importPath in models.modeledPaths) models.findPackage(importPath) else next() -} diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/ModelPathNormalizer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelPathTransformer.kt similarity index 51% rename from core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/ModelPathNormalizer.kt rename to core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelPathTransformer.kt index d2608f886d..c1f8af8a80 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/ModelPathNormalizer.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelPathTransformer.kt @@ -2,19 +2,21 @@ package org.opentaint.ir.go.client import org.opentaint.ir.go.api.* -internal data class ModelPathNormalizer( - val replacements: Map, - val targets: Set, - private val orderedModelPaths: List, -) { - fun path(value: String): String = replacements[value] ?: value +/** Maps model-addressed packages (`opentaint/`) and their symbols onto their target paths. */ +internal class GoIRModelPathTransformer private constructor( + private val replacements: Map, +) : GoIRTransformer { + val targets: Set = replacements.values.toSet() + private val orderedModelPaths = replacements.keys.sortedByDescending(String::length) - fun symbol(value: String): String = orderedModelPaths.fold(value) { result, modelPath -> + override fun packagePath(importPath: String): String = replacements[importPath] ?: importPath + + override fun symbolName(fullName: String): String = orderedModelPaths.fold(fullName) { result, modelPath -> result.replace(modelPath, replacements.getValue(modelPath)) } companion object { - fun create(program: GoIRProgram): ModelPathNormalizer { + fun create(program: GoIRProgram): GoIRModelPathTransformer { val replacements = program.packages.values .filterNot { it.isDependency || it.isStdlib || it.importPath == UNIVERSE_PACKAGE_PATH } .associate { pkg -> @@ -32,27 +34,10 @@ internal data class ModelPathNormalizer( "Go model contains no model-addressed packages, expected at least one package " + "under $GO_MODEL_PREFIX" } - return ModelPathNormalizer( - replacements, - replacements.values.toSet(), - replacements.keys.sortedByDescending(String::length), - ) + return GoIRModelPathTransformer(replacements) } } } private const val GO_MODEL_PREFIX = "opentaint/" private const val UNIVERSE_PACKAGE_PATH = "" - -internal class GoIRModelPathTransformer(private val normalizer: ModelPathNormalizer) : GoIRTransformer { - override fun packagePath(importPath: String) = normalizer.path(importPath) - override fun symbolName(fullName: String) = normalizer.symbol(fullName) - override fun resolvePackage(importPath: String, next: () -> GoIRPackage?): GoIRPackage? { - val pkg = next() ?: return null - if (pkg.importPath !in normalizer.replacements) return pkg - return object : GoIRPackage by pkg { - override val isStdlib = false - override val isDependency = true - } - } -} diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt index 3317ea7ced..f553d05db8 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt @@ -10,16 +10,12 @@ import org.opentaint.ir.go.impl.GoIRFunctionImpl import org.opentaint.ir.go.type.* import java.util.IdentityHashMap -internal data class SpecializedGoModelFunction( - val root: GoIRFunction, - val anonymousFunctions: List, -) - /** Substitutes concrete type arguments and closure lookups while reading a generic model body. */ internal class GoIRModelSpecializer( private val program: GoIRProgram, ) { - fun specialize(modelRoot: GoIRFunction, target: GoIRFunction): SpecializedGoModelFunction { + /** Returns the specialized root followed by its specialized anonymous functions. */ + fun specialize(modelRoot: GoIRFunction, target: GoIRFunction): List { require(modelRoot.bodyAvailable) { "Go model function ${modelRoot.fullName} has no body" } require(modelRoot.typeParams.size == target.typeArgs.size) { "Go generic instance ${target.fullName} has ${target.typeArgs.size} type arguments, " + @@ -117,7 +113,7 @@ internal class GoIRModelSpecializer( }) ?: error("Go model ${source.fullName} has no body") } } - return SpecializedGoModelFunction(root, sourceFunctions.map(functionMap::getValue)) + return listOf(root) + sourceFunctions.map(functionMap::getValue) } private fun collectAnonymousFunctions(root: GoIRFunction): List { diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt index a8bbf14b66..fd6d15d3c5 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt @@ -1,6 +1,10 @@ package org.opentaint.ir.go.client import org.opentaint.ir.go.api.* +import org.opentaint.ir.go.impl.GoIRConstImpl +import org.opentaint.ir.go.impl.GoIRGlobalImpl +import org.opentaint.ir.go.impl.functionEquals +import org.opentaint.ir.go.impl.functionHashCode import org.opentaint.ir.go.type.* /** Lookup view. Only finite package members and individual lookup results retain resolved values. */ @@ -55,12 +59,6 @@ class GoIRTransformerChain( } } - private fun global(source: GoIRGlobal): GoIRGlobal = object : GoIRGlobal by source { - override val pkg get() = findPackage(path(source.pkg.importPath))!! - override val fullName get() = symbol(source.fullName) - override val type by lazy { types(source.type) } - } - private fun reference(ref: GoIrFunctionReference): GoIrFunctionReference = ref.copy(pkg = path(ref.pkg), signature = types(ref.signature) as GoIRFuncType).also { it.program = this } @@ -107,8 +105,8 @@ class GoIRTransformerChain( else params + freeVars.map { GoIRParameter(it.name, it.type, params.size + it.index) } override val returnType get() = signature.results.first() override fun flowGraph() = body?.instGraph ?: error("Function $name has no body") - override fun equals(other: Any?): Boolean = other is GoIRFunction && fullName == other.fullName - override fun hashCode(): Int = fullName.hashCode() + override fun equals(other: Any?): Boolean = functionEquals(other) + override fun hashCode(): Int = functionHashCode() override fun toString(): String = source.toString() } @@ -122,14 +120,14 @@ class GoIRTransformerChain( override val importPath get() = path(source.importPath) override val functions by lazy { source.functions.map(::function) } override val namedTypes by lazy { source.namedTypes.map { NamedTypeView(it, this) } } - override val globals by lazy { source.globals.map(::global) } - override val constants by lazy { - source.constants.map { constant -> - object : GoIRConst by constant { - override val pkg get() = this@PackageView - override val fullName get() = symbol(constant.fullName) - override val type by lazy { types(constant.type) } - } + override val globals: List by lazy { + source.globals.map { + GoIRGlobalImpl(it.name, symbol(it.fullName), types(it.type), this, it.isExported, it.position) + } + } + override val constants: List by lazy { + source.constants.map { + GoIRConstImpl(it.name, symbol(it.fullName), types(it.type), it.value, this, it.isExported, it.position) } } override fun findGlobal(name: String) = globals.find { it.name == name } @@ -138,6 +136,9 @@ class GoIRTransformerChain( override val initFunction by lazy { source.initFunction?.let { reference(it.reference()).function } } override fun findNamedType(name: String) = namedTypes.find { it.name == name } override fun allMethods() = namedTypes.flatMap { it.allMethods() } + override fun equals(other: Any?): Boolean = other is GoIRPackage && importPath == other.importPath + override fun hashCode(): Int = importPath.hashCode() + override fun toString(): String = "GoIRPackage($importPath)" } private inner class NamedTypeView(private val source: GoIRNamedType, override val pkg: GoIRPackage) : @@ -156,6 +157,9 @@ class GoIRTransformerChain( } override fun allMethods() = methods + pointerMethods override fun methodByName(name: String) = allMethods().find { it.name == name } + override fun equals(other: Any?): Boolean = other is GoIRNamedType && fullName == other.fullName + override fun hashCode(): Int = fullName.hashCode() + override fun toString(): String = "GoIRNamedType($fullName)" } } diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionIdentity.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionIdentity.kt new file mode 100644 index 0000000000..52cced30c8 --- /dev/null +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionIdentity.kt @@ -0,0 +1,18 @@ +package org.opentaint.ir.go.impl + +import org.opentaint.ir.go.api.GoIRFunction +import org.opentaint.ir.go.api.GoIRModelFunction + +/** + * Function identity shared by every [GoIRFunction] implementation in this module. + * A model keeps the name of the function it replaces, so model-ness is part of the identity: + * otherwise bodies and instruction locations of a model and its original would compare equal. + */ +internal fun GoIRFunction.functionEquals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoIRFunction) return false + return fullName == other.fullName && (this is GoIRModelFunction) == (other is GoIRModelFunction) +} + +internal fun GoIRFunction.functionHashCode(): Int = + 31 * fullName.hashCode() + if (this is GoIRModelFunction) 1 else 0 diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt index d8e1f15256..c76af95e82 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt @@ -65,11 +65,7 @@ class GoIRFunctionImpl( override fun toString(): String = "GoIRFunction($fullName)" - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is GoIRFunction) return false - return fullName == other.fullName - } + override fun equals(other: Any?): Boolean = functionEquals(other) - override fun hashCode(): Int = fullName.hashCode() + override fun hashCode(): Int = functionHashCode() } diff --git a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt index 7d5fddbe3b..615340c145 100644 --- a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt +++ b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt @@ -129,6 +129,52 @@ class ModelTests { assertThat(originalPackage.functions.map { it.name }).doesNotContain("helper") } + @Test + fun `model views keep identity consistent with the original Go IR`(builder: GoIRTestBuilder) { + val project = project( + "example.com/original", + "package original\n" + + "type Box struct { Value string }\n" + + "var Global string\n" + + "const Constant = 1\n" + + "func Identity(value string) string { return \"original\" }\n" + + "func Untouched() {}\n", + ) + val model = model( + "example.com/original", + "package original\n" + + "type Box struct { Value string; extra string }\n" + + "func Identity(value string) string { return value }\n", + ) + + val effectiveProgram = builder.buildFromDir(project, modelConfig(model)) + val effective = effectiveProgram.findPackage("example.com/original")!! + val original = effectiveProgram.originalProgram.findPackage("example.com/original")!! + fun assertSameIdentity(first: Any, second: Any) { + assertThat(first).isEqualTo(second) + assertThat(second).isEqualTo(first) + assertThat(first.hashCode()).isEqualTo(second.hashCode()) + } + + assertSameIdentity(effective, original) + assertSameIdentity(effective.findNamedType("Box")!!, original.findNamedType("Box")!!) + assertSameIdentity(effective.findGlobal("Global")!!, original.findGlobal("Global")!!) + assertSameIdentity(effective.findConstant("Constant")!!, original.findConstant("Constant")!!) + assertSameIdentity( + effective.functions.single { it.name == "Untouched" }, + original.functions.single { it.name == "Untouched" }, + ) + assertThat(effective.findNamedType("Box")!!.fields.first()) + .isEqualTo(original.findNamedType("Box")!!.fields.first()) + + val approximation = effective.functions.single { it.name == "Identity" } + val originalIdentity = original.functions.single { it.name == "Identity" } + assertThat(approximation).isNotEqualTo(originalIdentity) + assertThat(originalIdentity).isNotEqualTo(approximation) + assertThat(approximation.body).isNotEqualTo(originalIdentity.body) + assertThat(setOf(approximation, originalIdentity)).hasSize(2) + } + @Test fun `partial model adds fields and methods after client deserialization`(builder: GoIRTestBuilder) { val project = Files.createTempDirectory("goir-model-fields-project") From f51e4de17a2d2d564ec547a8e2bee64e1ffc509d Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Wed, 28 Oct 2026 10:05:26 +0100 Subject: [PATCH 12/12] refactor(go-models): drop IR view wrappers Resolve models through package lookup on a single GoIRProgramImpl instead of a transformer-chain program that re-wrapped every function, package and named type. The model transformer now builds concrete IR objects (GoIRPackageImpl, GoIRNamedTypeImpl, GoIRModelFunctionImpl, GoIRApproximatedFunctionImpl) and maps opentaint/ paths while decoding model bodies, like the JVM approximation instruction substitutor. Removes GoIRTransformerChain, its function/package/named-type views, the delegating model function wrappers, GoIRModelPathTransformer and GoIRBodyReader. GoIRProgram.originalProgram is gone; originals are reached through GoIRApproximatedFunction.original. --- .../org/opentaint/ir/go/api/GoIRLookup.kt | 2 +- .../org/opentaint/ir/go/api/GoIRProgram.kt | 3 - .../opentaint/ir/go/api/GoIRTransformer.kt | 5 +- .../{GoIRBodyReader.kt => GoIRBodyContext.kt} | 7 +- .../ir/go/client/GoIRDeserializer.kt | 12 +- .../opentaint/ir/go/client/GoIRModelLoader.kt | 10 +- .../ir/go/client/GoIRModelOverlay.kt | 319 ------------ .../ir/go/client/GoIRModelPathTransformer.kt | 43 -- .../ir/go/client/GoIRModelSpecializer.kt | 61 +-- .../ir/go/client/GoIRModelTransformer.kt | 464 ++++++++++++++++++ .../ir/go/client/GoIRTransformerChain.kt | 236 --------- .../opentaint/ir/go/impl/GoIRFunctionImpl.kt | 8 +- .../ir/go/impl/GoIRModelFunctionImpl.kt | 62 +++ .../opentaint/ir/go/impl/GoIRProgramImpl.kt | 48 +- .../ir/go/test/features/ModelTests.kt | 115 ++--- 15 files changed, 652 insertions(+), 743 deletions(-) rename core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/{GoIRBodyReader.kt => GoIRBodyContext.kt} (59%) delete mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt delete mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelPathTransformer.kt create mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelTransformer.kt delete mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt create mode 100644 core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRModelFunctionImpl.kt diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRLookup.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRLookup.kt index ba25481903..99fd46a056 100644 --- a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRLookup.kt +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRLookup.kt @@ -2,7 +2,7 @@ package org.opentaint.ir.go.api import org.opentaint.ir.go.type.NamedTypeRef -/** Symbol resolution in one program view. References cache only their own lookup result. */ +/** Symbol resolution in one program. References cache only their own lookup result. */ interface GoIRLookup { fun function(reference: GoIrFunctionReference): GoIRFunction? fun namedType(reference: NamedTypeRef): GoIRNamedType? diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt index accb4add9c..6a360f0d42 100644 --- a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRProgram.kt @@ -6,9 +6,6 @@ import org.opentaint.ir.go.type.GoIRAnonymousInterfaceType * Top-level entity: a loaded Go program with all its packages. */ interface GoIRProgram { - /** The program before model lookup overlays. This program is [this] when no models are active. */ - val originalProgram: GoIRProgram get() = this - val lookup: GoIRLookup get() = GoIRProgramLookup(this) val packages: Map // keyed by import path diff --git a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRTransformer.kt b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRTransformer.kt index 72b8f7783d..9ca8c3b440 100644 --- a/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRTransformer.kt +++ b/core/opentaint-ir/go/go-ir-api/src/main/kotlin/org/opentaint/ir/go/api/GoIRTransformer.kt @@ -1,10 +1,7 @@ package org.opentaint.ir.go.api -/** A lookup interceptor. Unhandled requests are passed to the next resolver. */ +/** A package lookup interceptor. Unhandled requests are passed to the next resolver. */ interface GoIRTransformer { fun resolvePackage(importPath: String, next: () -> GoIRPackage?): GoIRPackage? = next() - fun resolveFunction(reference: GoIrFunctionReference, next: () -> GoIRFunction?): GoIRFunction? = next() - fun packagePath(importPath: String): String = importPath - fun symbolName(fullName: String): String = fullName val additionalPackages: List get() = emptyList() } diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyReader.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyContext.kt similarity index 59% rename from core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyReader.kt rename to core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyContext.kt index 39425c4450..be618934cd 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyReader.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRBodyContext.kt @@ -3,12 +3,7 @@ package org.opentaint.ir.go.client import org.opentaint.ir.go.api.* import org.opentaint.ir.go.type.* -/** Decodes a body directly in its owner’s lookup context, without an intermediate IR copy. */ -interface GoIRBodyReader { - fun readBody(owner: GoIRFunction, context: GoIRBodyContext): GoIRBody? -} - -/** Composable resolution policy supplied by the program view, independent of wire decoding. */ +/** Symbol resolution policy applied while a body is decoded, independent of wire decoding. */ open class GoIRBodyContext { open fun type(type: GoIRType): GoIRType = type open fun function(reference: GoIrFunctionReference): GoIrFunctionReference = reference diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt index fdebf59494..c4a86c87e4 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRDeserializer.kt @@ -164,18 +164,18 @@ class GoIRDeserializer { } internal data class DeserializedGoIRBuild( - val program: GoIRProgram, + val program: GoIRProgramImpl, val models: List, ) internal fun deserializeProgram( serializedProgram: ProtoProgram, - ): GoIRProgram { + ): GoIRProgramImpl { val resultPackages = hashMapOf() val anonymousInterfaces = Int2ObjectOpenHashMap() val result = GoIRProgramImpl(resultPackages, anonymousInterfaces) - programView = result + deserializedProgram = result val pkgInfo = deserializePackages(serializedProgram.packagesList) serializedProgram.packagesList.forEach { serializedPackage -> @@ -1073,12 +1073,12 @@ class GoIRDeserializer { } private fun functionReference(function: GoIRFunction): GoIrFunctionReference = - function.reference().also { it.program = programView } + function.reference().also { it.program = deserializedProgram } private fun globalReference(id: Int): GoIRGlobalReference = - resolveGlobal(id).reference().also { it.program = programView } + resolveGlobal(id).reference().also { it.program = deserializedProgram } - private lateinit var programView: GoIRProgram + private lateinit var deserializedProgram: GoIRProgram private fun resolveGlobal(globalId: Int): GoIRGlobalImpl = globalsById.get(globalId) diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt index e7dd45be84..d063838041 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelLoader.kt @@ -7,14 +7,14 @@ internal object GoIRModelLoader { fun apply(build: GoIRDeserializer.DeserializedGoIRBuild): GoIRProgram { if (build.models.isEmpty()) return build.program val definitions = build.models.map { model -> - val rawProgram = GoIRDeserializer().deserializeProgram(model.program) - val paths = try { - GoIRModelPathTransformer.create(rawProgram) + val program = GoIRDeserializer().deserializeProgram(model.program) + try { + GoIRModelDefinition.create(model.source, program) } catch (error: IllegalArgumentException) { throw IllegalArgumentException("invalid Go model ${model.source}: ${error.message}", error) } - GoIRModelDefinition(model.source, GoIRTransformerChain(rawProgram, listOf(paths)), paths.targets) } - return GoIRTransformerChain(build.program, listOf(GoIRModelTransformer(build.program, definitions))) + build.program.install(listOf(GoIRModelTransformer(build.program, definitions))) + return build.program } } diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt deleted file mode 100644 index 37d1fdcc45..0000000000 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelOverlay.kt +++ /dev/null @@ -1,319 +0,0 @@ -package org.opentaint.ir.go.client - -import org.opentaint.ir.go.api.GoIRApproximatedFunction -import org.opentaint.ir.go.api.GoIRBody -import org.opentaint.ir.go.api.GoIRFunction -import org.opentaint.ir.go.api.GoIRModelFunction -import org.opentaint.ir.go.api.GoIRNamedType -import org.opentaint.ir.go.api.GoIRPackage -import org.opentaint.ir.go.api.GoIRProgram -import org.opentaint.ir.go.api.GoIRTransformer -import org.opentaint.ir.go.impl.GoIRNamedTypeImpl -import org.opentaint.ir.go.impl.GoIRPackageImpl -import org.opentaint.ir.go.impl.functionEquals -import org.opentaint.ir.go.impl.functionHashCode -import org.opentaint.ir.go.type.GoIRAnonymousInterfaceType -import org.opentaint.ir.go.type.GoIRAnonymousInterfaceTypeRef -import org.opentaint.ir.go.type.GoIRArrayType -import org.opentaint.ir.go.type.GoIRBasicType -import org.opentaint.ir.go.type.GoIRChanType -import org.opentaint.ir.go.type.GoIRFuncType -import org.opentaint.ir.go.type.GoIRMapType -import org.opentaint.ir.go.type.GoIRNamedInterfaceType -import org.opentaint.ir.go.type.GoIRNamedTypeRef -import org.opentaint.ir.go.type.GoIRPointerType -import org.opentaint.ir.go.type.GoIRSliceType -import org.opentaint.ir.go.type.GoIRStructField -import org.opentaint.ir.go.type.GoIRStructType -import org.opentaint.ir.go.type.GoIRTupleType -import org.opentaint.ir.go.type.GoIRType -import org.opentaint.ir.go.type.GoIRTypeParamRef -import org.opentaint.ir.go.type.GoIRTypeParamType - -internal data class GoIRModelDefinition( - val source: String, - val program: GoIRProgram, - val targetPaths: Set, -) - -/** Applies partial Go models by intercepting the lookup of every modeled package. */ -internal class GoIRModelTransformer( - private val originalProgram: GoIRProgram, - definitions: List, -) : GoIRTransformer { - private val definitionsByTarget = linkedMapOf() - private val modelDependencies = linkedMapOf() - - init { - definitions.forEach { definition -> - definition.targetPaths.forEach { target -> - val previous = definitionsByTarget.putIfAbsent(target, definition) - require(previous == null) { - "Go package \"$target\" is modeled more than once " + - "(${previous?.source} and ${definition.source})" - } - } - } - // Bundled modules can describe dependencies that this project does not use. - definitionsByTarget.entries.removeIf { (target, _) -> originalProgram.findPackage(target) == null } - definitionsByTarget.forEach { (target, definition) -> - validatePackage(target, definition) - } - definitions.forEach { definition -> - if (definition.targetPaths.none { it in definitionsByTarget }) return@forEach - definition.program.packages.forEach { (path, pkg) -> - if (path !in definition.targetPaths && originalProgram.findPackage(path) == null) { - modelDependencies.putIfAbsent(path, pkg) - } - } - } - } - - override val additionalPackages: List get() = modelDependencies.values.toList() - - override fun resolvePackage(importPath: String, next: () -> GoIRPackage?): GoIRPackage? { - val definition = definitionsByTarget[importPath] ?: return next() - val original = next() ?: return null - return createPackage(original, definition.program.findPackage(importPath)!!) - } - - private fun validatePackage(target: String, definition: GoIRModelDefinition) { - val original = originalProgram.findPackage(target) - ?: throw IllegalArgumentException( - "invalid Go model ${definition.source}: target package \"$target\" was not found", - ) - val model = definition.program.findPackage(target) - ?: throw IllegalArgumentException( - "invalid Go model ${definition.source}: model package \"$target\" was not found", - ) - require(model.name == original.name) { - "Go model package \"opentaint/$target\" has package name " + - "\"${model.name}\", want \"${original.name}\" (the modeled package name)" - } - - val originalFunctions = original.functions.associateBy { it.fullName } - val initializers = initializerFunctions(model) - model.functions.forEach { function -> - if (function in initializers || function.parent != null) return@forEach - val targetFunction = originalFunctions[function.fullName] - if (targetFunction != null) { - require(sameSignature(function, targetFunction)) { - "Go model function ${function.fullName} has a different signature from its target" - } - } - } - validateFields(original, model, definition.source) - } - - private fun createPackage(original: GoIRPackage, model: GoIRPackage): GoIRPackage { - val initializers = initializerFunctions(model) - val modelFunctions = model.functions.filterNot { it in initializers } - val modelByName = modelFunctions - .filter { it.parent == null } - .associateBy { it.fullName } - val used = mutableSetOf() - val specializedAnonymousFunctions = mutableListOf() - val specializer = GoIRModelSpecializer(originalProgram) - val result = GoIRPackageImpl(original.importPath, original.name, original.isStdlib, original.isDependency) - original.functions.forEach { target -> - val replacement = modelByName[target.fullName] - val genericModel = target.originFullName?.let(modelByName::get) - result.addFunction( - if (replacement != null && replacement.bodyAvailable && sameSignature(replacement, target)) { - used += replacement - ApproximatedFunction(replacement, target) - } else if (genericModel != null && genericModel.bodyAvailable) { - val specialized = specializer.specialize(genericModel, target) - specializedAnonymousFunctions += specialized.drop(1) - ApproximatedFunction(specialized.first(), target) - } else { - target - }, - ) - } - specializedAnonymousFunctions.forEach { result.addFunction(ModelFunction(it)) } - modelFunctions.filter { it !in used }.forEach { result.addFunction(ModelFunction(it)) } - - val functionsByName = buildMap { result.functions.forEach { putIfAbsent(it.fullName, it) } } - val modelTypes = model.namedTypes.associateBy { it.name } - original.namedTypes.forEach { target -> - val modelType = modelTypes[target.name] - result.addNamedType(modelType?.let { mergeNamedType(target, it, result, functionsByName) } ?: target) - } - model.namedTypes.filter { original.findNamedType(it.name) == null }.forEach(result::addNamedType) - (original.globals + model.globals).forEach { if (result.findGlobal(it.name) == null) result.addGlobal(it) } - (original.constants + model.constants).forEach { if (result.findConstant(it.name) == null) result.addConst(it) } - (original.imports + model.imports).distinctBy { it.importPath }.forEach { result._imports += it } - result.initFunction = original.initFunction - return result - } - - private fun mergeNamedType( - original: GoIRNamedType, - model: GoIRNamedType, - pkg: GoIRPackage, - functionsByName: Map, - ): GoIRNamedType { - val originalNames = original.fields.mapTo(mutableSetOf()) { it.name } - val fields = original.fields + model.fields.filter { it.name !in originalNames } - .mapIndexed { index, field -> field.copy(index = original.fields.size + index) } - val originalStruct = original.underlying as? GoIRStructType - val underlying = originalStruct?.let { struct -> - GoIRStructType( - fields.map { GoIRStructField(it.name, it.type, it.isEmbedded, it.tag) }, - struct.structTypeRef, - ) - } ?: original.underlying - val result = GoIRNamedTypeImpl( - original.name, original.fullName, pkg, underlying, original.kind, original.position, original.typeParams, - ) - fields.mapTo(result._fields) { it.copy(enclosingType = result) } - effectiveMethods(original.methods, model.methods, functionsByName).let(result._methods::addAll) - effectiveMethods(original.pointerMethods, model.pointerMethods, functionsByName) - .let(result._pointerMethods::addAll) - result._interfaceMethods += original.interfaceMethods - result._embeddedInterfaces += original.embeddedInterfaces - return result - } - - private fun effectiveMethods( - targetMethods: List, - modelMethods: List, - functionsByName: Map, - ): List { - val targetNames = targetMethods.mapTo(mutableSetOf()) { it.fullName } - return (targetMethods + modelMethods.filter { it.fullName !in targetNames }) - .map { functionsByName[it.fullName] ?: it } - } - - private fun validateFields(original: GoIRPackage, model: GoIRPackage, source: String) { - model.namedTypes.forEach { modelType -> - val targetType = original.findNamedType(modelType.name) ?: return@forEach - val targetFields = targetType.fields.associateBy { it.name } - modelType.fields.forEach { modelField -> - val targetField = targetFields[modelField.name] ?: return@forEach - require(sameType(modelField.type, targetField.type)) { - "invalid Go model $source: field ${targetType.fullName}.${modelField.name} " + - "has a different type from its target" - } - } - } - } - - private fun sameSignature(model: GoIRFunction, target: GoIRFunction): Boolean = - sameType(model.signature, target.signature) && - model.isMethod == target.isMethod && - model.isPointerReceiver == target.isPointerReceiver - - private fun sameType(first: GoIRType, second: GoIRType): Boolean = - sameType(first, second, mutableListOf()) - - private fun sameType( - first: GoIRType, - second: GoIRType, - active: MutableList>, - ): Boolean { - if (first === second) return true - if (active.any { (left, right) -> left === first && right === second }) return true - active += first to second - return when { - first is GoIRBasicType && second is GoIRBasicType -> first.kind == second.kind - first is GoIRPointerType && second is GoIRPointerType -> - sameType(first.elem, second.elem, active) - first is GoIRArrayType && second is GoIRArrayType -> - first.length == second.length && sameType(first.elem, second.elem, active) - first is GoIRSliceType && second is GoIRSliceType -> - sameType(first.elem, second.elem, active) - first is GoIRMapType && second is GoIRMapType -> - sameType(first.key, second.key, active) && sameType(first.value, second.value, active) - first is GoIRChanType && second is GoIRChanType -> - first.direction == second.direction && sameType(first.elem, second.elem, active) - first is GoIRStructType && second is GoIRStructType -> - first.structTypeRef?.fullRefName == second.structTypeRef?.fullRefName && - first.fields.size == second.fields.size && - first.fields.zip(second.fields).all { (left, right) -> - left.name == right.name && left.isEmbedded == right.isEmbedded && - left.tag == right.tag && sameType(left.type, right.type, active) - } - first is GoIRNamedTypeRef && second is GoIRNamedTypeRef -> - first.typeRef.fullRefName == second.typeRef.fullRefName && - sameTypes(first.typeArgs, second.typeArgs, active) - first is GoIRFuncType && second is GoIRFuncType -> - first.isVariadic == second.isVariadic && - nullableTypeEqual(first.recv, second.recv, active) && - sameTypes(first.params, second.params, active) && - sameTypes(first.results, second.results, active) - first is GoIRTupleType && second is GoIRTupleType -> - sameTypes(first.elements, second.elements, active) - first is GoIRTypeParamType && second is GoIRTypeParamType -> - first.paramIndex == second.paramIndex && first.name == second.name && - sameType(first.constraint, second.constraint, active) - first is GoIRTypeParamRef && second is GoIRTypeParamRef -> - first.paramIndex == second.paramIndex && first.name == second.name - first is GoIRAnonymousInterfaceTypeRef && second is GoIRAnonymousInterfaceTypeRef -> - sameAnonymousInterface(first.interfaceType, second.interfaceType, active) - first is GoIRAnonymousInterfaceType && second is GoIRAnonymousInterfaceType -> - sameAnonymousInterface(first, second, active) - first is GoIRNamedInterfaceType && second is GoIRNamedInterfaceType -> - first.interfaceTypeRef.fullRefName == second.interfaceTypeRef.fullRefName - else -> false - } - } - - private fun sameAnonymousInterface( - first: GoIRAnonymousInterfaceType, - second: GoIRAnonymousInterfaceType, - active: MutableList>, - ): Boolean = first.methods.size == second.methods.size && - first.methods.zip(second.methods).all { (left, right) -> - left.name == right.name && sameType(left.signature, right.signature, active) - } && sameTypes(first.embeds, second.embeds, active) - - private fun sameTypes( - first: List, - second: List, - active: MutableList>, - ): Boolean = first.size == second.size && - first.zip(second).all { (left, right) -> sameType(left, right, active) } - - private fun nullableTypeEqual( - first: GoIRType?, - second: GoIRType?, - active: MutableList>, - ): Boolean = when { - first == null -> second == null - second == null -> false - else -> sameType(first, second, active) - } - - private fun initializerFunctions(pkg: GoIRPackage): Set { - val result = pkg.functions - .filterTo(mutableSetOf()) { it.name == "init" || it.name.startsWith("init#") } - do { - val oldSize = result.size - pkg.functions.forEach { function -> - val parentName = function.parent?.fullName - if (parentName != null && result.any { it.fullName == parentName }) { - result += function - } - } - } while (result.size != oldSize) - return result - } -} - - -private open class ModelFunction(protected val implementation: GoIRFunction) : GoIRModelFunction, - GoIRFunction by implementation, GoIRBodyReader { - override fun readBody(owner: GoIRFunction, context: GoIRBodyContext): GoIRBody? = - (implementation as GoIRBodyReader).readBody(owner, context) - override val isSynthetic: Boolean get() = true - override fun equals(other: Any?): Boolean = functionEquals(other) - override fun hashCode(): Int = functionHashCode() - override fun toString(): String = "GoIRModelFunction($fullName)" -} - -private class ApproximatedFunction( - implementation: GoIRFunction, - override val original: GoIRFunction, -) : ModelFunction(implementation), GoIRApproximatedFunction diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelPathTransformer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelPathTransformer.kt deleted file mode 100644 index c1f8af8a80..0000000000 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelPathTransformer.kt +++ /dev/null @@ -1,43 +0,0 @@ -package org.opentaint.ir.go.client - -import org.opentaint.ir.go.api.* - -/** Maps model-addressed packages (`opentaint/`) and their symbols onto their target paths. */ -internal class GoIRModelPathTransformer private constructor( - private val replacements: Map, -) : GoIRTransformer { - val targets: Set = replacements.values.toSet() - private val orderedModelPaths = replacements.keys.sortedByDescending(String::length) - - override fun packagePath(importPath: String): String = replacements[importPath] ?: importPath - - override fun symbolName(fullName: String): String = orderedModelPaths.fold(fullName) { result, modelPath -> - result.replace(modelPath, replacements.getValue(modelPath)) - } - - companion object { - fun create(program: GoIRProgram): GoIRModelPathTransformer { - val replacements = program.packages.values - .filterNot { it.isDependency || it.isStdlib || it.importPath == UNIVERSE_PACKAGE_PATH } - .associate { pkg -> - require(pkg.importPath.startsWith(GO_MODEL_PREFIX)) { - "Go model package \"${pkg.importPath}\" is not model-addressed, " + - "its import path must be ${GO_MODEL_PREFIX}" - } - val target = pkg.importPath.removePrefix(GO_MODEL_PREFIX) - require(target.isNotEmpty()) { - "Go model package \"${pkg.importPath}\" has an empty target package" - } - pkg.importPath to target - } - require(replacements.isNotEmpty()) { - "Go model contains no model-addressed packages, expected at least one package " + - "under $GO_MODEL_PREFIX" - } - return GoIRModelPathTransformer(replacements) - } - } -} - -private const val GO_MODEL_PREFIX = "opentaint/" -private const val UNIVERSE_PACKAGE_PATH = "" diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt index f553d05db8..cc81aba59a 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelSpecializer.kt @@ -1,6 +1,8 @@ package org.opentaint.ir.go.client import org.opentaint.ir.go.api.GoIRFunction +import org.opentaint.ir.go.api.GoIRGlobalReference +import org.opentaint.ir.go.api.GoIRPackage import org.opentaint.ir.go.api.GoIRParameter import org.opentaint.ir.go.api.GoIRFreeVar import org.opentaint.ir.go.api.GoIRProgram @@ -13,6 +15,9 @@ import java.util.IdentityHashMap /** Substitutes concrete type arguments and closure lookups while reading a generic model body. */ internal class GoIRModelSpecializer( private val program: GoIRProgram, + private val pkg: GoIRPackage, + private val model: GoIRBodyContext, + private val symbol: (String) -> String, ) { /** Returns the specialized root followed by its specialized anonymous functions. */ fun specialize(modelRoot: GoIRFunction, target: GoIRFunction): List { @@ -24,7 +29,8 @@ internal class GoIRModelSpecializer( val typeArguments = modelRoot.typeParams.associate { parameter -> parameter.index to target.typeArgs[parameter.index] } - val specializeType = TypeSpecializer(typeArguments) + val specializer = TypeSpecializer(typeArguments) + fun specializeType(type: GoIRType) = specializer(model.type(type)) val sourceFunctions = collectAnonymousFunctions(modelRoot) val suffix = target.name.removePrefix(modelRoot.name).ifEmpty { "#opentaint-model-${target.fullName.hashCode().toUInt()}" @@ -33,14 +39,15 @@ internal class GoIRModelSpecializer( val anonymousRefs = IdentityHashMap>() fun reference(function: GoIRFunction): GoIrFunctionReference = - GoIrFunctionReference(function.pkg?.importPath.orEmpty(), function.name, function.signature) + GoIrFunctionReference(pkg.importPath, function.name, function.signature) .also { it.program = program } val rootRefs = mutableListOf() - val root = GoIRFunctionImpl( + val root = modelFunction( + original = target, name = target.name, fullName = target.fullName, - pkg = target.pkg, + pkg = pkg, signature = target.signature, params = target.params, freeVars = target.freeVars, @@ -48,9 +55,8 @@ internal class GoIRModelSpecializer( isMethod = target.isMethod, isPointerReceiver = target.isPointerReceiver, isExported = target.isExported, - isSynthetic = true, syntheticKind = modelRoot.syntheticKind, - declaredHasBody = true, + hasBody = true, parent = target.parent, anonymousFunctions = rootRefs, typeParams = target.typeParams, @@ -64,26 +70,27 @@ internal class GoIRModelSpecializer( val refs = mutableListOf() val parent = source.parent?.function?.let(functionMap::get) ?: error("Go model anonymous function ${source.fullName} has no copied parent") - val signature = specializeType(source.signature) as GoIRFuncType - val copied = GoIRFunctionImpl( + val copied = modelFunction( + original = null, name = source.name + suffix, - fullName = source.fullName + suffix, - pkg = target.pkg, - signature = signature, + fullName = symbol(source.fullName) + suffix, + pkg = pkg, + signature = specializeType(source.signature) as GoIRFuncType, params = source.params.map { GoIRParameter(it.name, specializeType(it.type), it.index) }, freeVars = source.freeVars.map { GoIRFreeVar(it.name, specializeType(it.type), it.index) }, position = source.position, isMethod = source.isMethod, isPointerReceiver = source.isPointerReceiver, isExported = source.isExported, - isSynthetic = true, syntheticKind = source.syntheticKind, - declaredHasBody = true, + hasBody = true, parent = reference(parent), anonymousFunctions = refs, typeParams = source.typeParams.map { GoIRTypeParamDecl(it.name, it.index, specializeType(it.constraint)) }, + originFullName = null, + typeArgs = emptyList(), ) functionMap[source] = copied anonymousRefs[source] = refs @@ -92,25 +99,23 @@ internal class GoIRModelSpecializer( (listOf(modelRoot) + sourceFunctions).forEach { source -> val refs = anonymousRefs.getValue(source) refs += source.anonymousFunctions.map { child -> - reference(functionMap[child.function] ?: child.function) + functionMap[child.function]?.let(::reference) ?: model.function(child) } } + val context = object : GoIRBodyContext() { + override fun type(type: GoIRType) = specializeType(type) + override fun function(reference: GoIrFunctionReference): GoIrFunctionReference = + reference.program.lookup.function(reference)?.let(functionMap::get)?.let(::reference) + ?: model.function(reference) + override fun global(reference: GoIRGlobalReference) = model.global(reference) + override fun fieldIndex(type: GoIRType, index: Int, name: String) = + model.fieldIndex(type, index, name) + } (listOf(modelRoot) + sourceFunctions).forEach { source -> - val copied = functionMap.getValue(source) - val reader = source as? GoIRBodyReader - ?: error("Go model ${source.fullName} has no body reader") - copied.setBodyReader { owner, context -> - reader.readBody(owner, object : GoIRBodyContext() { - override fun type(type: GoIRType) = context.type(specializeType(type)) - override fun function(ref: GoIrFunctionReference): GoIrFunctionReference { - val replacement = functionMap[ref.function] - return context.function(if (replacement == null) ref else reference(replacement)) - } - override fun global(ref: org.opentaint.ir.go.api.GoIRGlobalReference) = context.global(ref) - override fun fieldIndex(type: GoIRType, index: Int, name: String) = - context.fieldIndex(specializeType(type), index, name) - }) ?: error("Go model ${source.fullName} has no body") + functionMap.getValue(source).setBodyReader { owner, _ -> + (source as GoIRFunctionImpl).readBody(owner, context) + ?: error("Go model ${source.fullName} has no body") } } return listOf(root) + sourceFunctions.map(functionMap::getValue) diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelTransformer.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelTransformer.kt new file mode 100644 index 0000000000..5261460852 --- /dev/null +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRModelTransformer.kt @@ -0,0 +1,464 @@ +package org.opentaint.ir.go.client + +import org.opentaint.ir.go.api.GoIRConst +import org.opentaint.ir.go.api.GoIRFunction +import org.opentaint.ir.go.api.GoIRGlobal +import org.opentaint.ir.go.api.GoIRGlobalReference +import org.opentaint.ir.go.api.GoIRNamedType +import org.opentaint.ir.go.api.GoIRPackage +import org.opentaint.ir.go.api.GoIRProgram +import org.opentaint.ir.go.api.GoIRTransformer +import org.opentaint.ir.go.api.GoIrFunctionReference +import org.opentaint.ir.go.impl.GoIRApproximatedFunctionImpl +import org.opentaint.ir.go.impl.GoIRConstImpl +import org.opentaint.ir.go.impl.GoIRFunctionImpl +import org.opentaint.ir.go.impl.GoIRGlobalImpl +import org.opentaint.ir.go.impl.GoIRModelFunctionImpl +import org.opentaint.ir.go.impl.GoIRNamedTypeImpl +import org.opentaint.ir.go.impl.GoIRPackageImpl +import org.opentaint.ir.go.impl.GoIRProgramImpl +import org.opentaint.ir.go.type.* + +/** A deserialized model module and the target path of each of its model-addressed (`opentaint/`) packages. */ +internal class GoIRModelDefinition private constructor( + val source: String, + val program: GoIRProgram, + val targets: Map, +) { + companion object { + fun create(source: String, program: GoIRProgram): GoIRModelDefinition { + val targets = program.packages.values + .filterNot { it.isDependency || it.isStdlib || it.importPath == UNIVERSE_PACKAGE_PATH } + .associate { pkg -> + require(pkg.importPath.startsWith(GO_MODEL_PREFIX)) { + "Go model package \"${pkg.importPath}\" is not model-addressed, " + + "its import path must be ${GO_MODEL_PREFIX}" + } + val target = pkg.importPath.removePrefix(GO_MODEL_PREFIX) + require(target.isNotEmpty()) { + "Go model package \"${pkg.importPath}\" has an empty target package" + } + pkg.importPath to target + } + require(targets.isNotEmpty()) { + "Go model contains no model-addressed packages, expected at least one package " + + "under $GO_MODEL_PREFIX" + } + return GoIRModelDefinition(source, program, targets) + } + } +} + +/** Applies partial Go models by intercepting the lookup of every modeled package. */ +internal class GoIRModelTransformer( + private val program: GoIRProgramImpl, + definitions: List, +) : GoIRTransformer { + private val scopesByTarget = linkedMapOf() + private val modelDependencies = linkedMapOf() + + init { + val definitionsByTarget = linkedMapOf() + definitions.forEach { definition -> + definition.targets.values.forEach { target -> + val previous = definitionsByTarget.putIfAbsent(target, definition) + require(previous == null) { + "Go package \"$target\" is modeled more than once " + + "(${previous?.source} and ${definition.source})" + } + } + } + // Bundled modules can describe dependencies that this project does not use. + definitionsByTarget.entries.removeIf { (target, _) -> program.declaredPackage(target) == null } + val scopes = definitionsByTarget.values.distinct().associateWith { ModelScope(it) } + definitionsByTarget.forEach { (target, definition) -> scopesByTarget[target] = scopes.getValue(definition) } + scopesByTarget.forEach { (target, scope) -> scope.validatePackage(target) } + scopes.keys.forEach { definition -> + definition.program.packages.forEach { (path, pkg) -> + if (path !in definition.targets && program.declaredPackage(path) == null) { + modelDependencies.putIfAbsent(path, pkg) + } + } + } + } + + override val additionalPackages: List get() = modelDependencies.values.toList() + + override fun resolvePackage(importPath: String, next: () -> GoIRPackage?): GoIRPackage? { + val scope = scopesByTarget[importPath] ?: return next() + val original = next() ?: return null + return scope.createPackage(original) + } + + /** Resolves the symbols of one model module into the program it models. */ + private inner class ModelScope(val definition: GoIRModelDefinition) { + private val modelPaths = definition.targets.keys.sortedByDescending(String::length) + private val modelPackages = definition.targets.entries.associate { (model, target) -> + target to definition.program.findPackage(model)!! + } + + // Anonymous interface IDs are local to each wire program, so model definitions get fresh program IDs. + private val interfaceIds: Map = run { + var nextId = (program.anonymousInterfaces.keys.maxOrNull() ?: -1) + 1 + definition.program.anonymousInterfaces.keys.associateWith { nextId++ } + } + + init { + definition.program.anonymousInterfaces.values.forEach { + program.addAnonymousInterface(type(it) as GoIRAnonymousInterfaceType) + } + } + + private fun path(importPath: String) = definition.targets[importPath] ?: importPath + + private fun symbol(fullName: String) = modelPaths.fold(fullName) { result, modelPath -> + result.replace(modelPath, definition.targets.getValue(modelPath)) + } + + private fun reference(ref: NamedTypeRef) = + ref.copy(refTypePkg = path(ref.refTypePkg)).also { it.program = program } + + fun type(value: GoIRType): GoIRType = when (value) { + is GoIRTypeParamType -> value.copy(constraint = type(value.constraint)) + is GoIRTypeParamRef -> value + is GoIRPointerType -> GoIRPointerType(type(value.elem)) + is GoIRArrayType -> GoIRArrayType(type(value.elem), value.length) + is GoIRSliceType -> GoIRSliceType(type(value.elem)) + is GoIRMapType -> GoIRMapType(type(value.key), type(value.value)) + is GoIRChanType -> GoIRChanType(type(value.elem), value.direction) + is GoIRStructType -> GoIRStructType( + value.fields.map { GoIRStructField(it.name, type(it.type), it.isEmbedded, it.tag) }, + value.structTypeRef?.let(::reference), + ) + is GoIRNamedTypeRef -> GoIRNamedTypeRef(reference(value.typeRef), value.typeArgs.map(::type)) + is GoIRNamedInterfaceType -> GoIRNamedInterfaceType( + value.methods.map { GoIRInterfaceMethodSig(it.name, type(it.signature) as GoIRFuncType) }, + value.embeds.map(::type), + reference(value.interfaceTypeRef), + ) + is GoIRAnonymousInterfaceTypeRef -> GoIRAnonymousInterfaceTypeRef(interfaceIds.getValue(value.id)) + .also { it.program = program } + is GoIRAnonymousInterfaceType -> GoIRAnonymousInterfaceType( + interfaceIds.getValue(value.id), + value.methods.map { GoIRInterfaceMethodSig(it.name, type(it.signature) as GoIRFuncType) }, + value.embeds.map(::type), + ) + is GoIRFuncType -> GoIRFuncType( + value.params.map(::type), + value.results.map(::type), + value.isVariadic, + value.recv?.let(::type), + ) + is GoIRTupleType -> GoIRTupleType(value.elements.map(::type)) + else -> value + } + + /** Decodes model bodies against the modeled program: target paths, program IDs and merged field layouts. */ + val context = object : GoIRBodyContext() { + override fun type(type: GoIRType) = this@ModelScope.type(type) + override fun function(reference: GoIrFunctionReference) = reference + .copy(pkg = path(reference.pkg), signature = type(reference.signature) as GoIRFuncType) + .also { it.program = program } + override fun global(reference: GoIRGlobalReference) = + reference.copy(pkg = path(reference.pkg)).also { it.program = program } + override fun fieldIndex(type: GoIRType, index: Int, name: String): Int { + fun named(type: GoIRType): GoIRNamedType? = when (type) { + is GoIRPointerType -> named(type.elem) + is GoIRNamedTypeRef -> type.namedType + is GoIRStructType -> type.namedType + else -> null + } + return named(type)?.fields?.firstOrNull { it.name == name }?.index ?: index + } + } + + private fun function(model: GoIRFunction, pkg: GoIRPackage, original: GoIRFunction?): GoIRFunction = + modelFunction( + original, model.name, symbol(model.fullName), pkg, type(model.signature) as GoIRFuncType, + model.params.map { it.copy(type = type(it.type)) }, + model.freeVars.map { it.copy(type = type(it.type)) }, + model.position, model.isMethod, model.isPointerReceiver, model.isExported, model.syntheticKind, + model.hasBody, model.parent?.let(context::function), model.anonymousFunctions.map(context::function), + model.typeParams.map { it.copy(constraint = type(it.constraint)) }, + model.originFullName?.let(::symbol), model.typeArgs.map(::type), + ).also { function -> + if (!model.bodyAvailable) return@also + function.setBodyReader { owner, _ -> + (model as GoIRFunctionImpl).readBody(owner, context) + ?: error("Go model ${model.fullName} has no body") + } + } + + fun validatePackage(target: String) { + val original = program.declaredPackage(target)!! + val model = modelPackages[target] + ?: throw IllegalArgumentException( + "invalid Go model ${definition.source}: model package \"$target\" was not found", + ) + require(model.name == original.name) { + "Go model package \"opentaint/$target\" has package name " + + "\"${model.name}\", want \"${original.name}\" (the modeled package name)" + } + + val originalFunctions = original.functions.associateBy { it.fullName } + val initializers = initializerFunctions(model) + model.functions.forEach { function -> + if (function in initializers || function.parent != null) return@forEach + val targetFunction = originalFunctions[symbol(function.fullName)] + if (targetFunction != null) { + require(sameSignature(function, targetFunction)) { + "Go model function ${symbol(function.fullName)} has a different signature from its target" + } + } + } + model.namedTypes.forEach { modelType -> + val targetType = original.findNamedType(modelType.name) ?: return@forEach + val targetFields = targetType.fields.associateBy { it.name } + modelType.fields.forEach { modelField -> + val targetField = targetFields[modelField.name] ?: return@forEach + require(sameType(type(modelField.type), targetField.type)) { + "invalid Go model ${definition.source}: field ${targetType.fullName}.${modelField.name} " + + "has a different type from its target" + } + } + } + } + + fun createPackage(original: GoIRPackage): GoIRPackage { + val model = modelPackages.getValue(original.importPath) + val initializers = initializerFunctions(model) + val modelFunctions = model.functions.filterNot { it in initializers } + val modelByName = modelFunctions + .filter { it.parent == null } + .associateBy { symbol(it.fullName) } + val used = mutableSetOf() + val specializedAnonymousFunctions = mutableListOf() + val result = GoIRPackageImpl(original.importPath, original.name, original.isStdlib, original.isDependency) + val specializer = GoIRModelSpecializer(program, result, context, ::symbol) + original.functions.forEach { target -> + val replacement = modelByName[target.fullName] + val genericModel = target.originFullName?.let(modelByName::get) + result.addFunction( + if (replacement != null && replacement.bodyAvailable && sameSignature(replacement, target)) { + used += replacement + function(replacement, result, target) + } else if (genericModel != null && genericModel.bodyAvailable) { + val specialized = specializer.specialize(genericModel, target) + specializedAnonymousFunctions += specialized.drop(1) + specialized.first() + } else { + target + }, + ) + } + specializedAnonymousFunctions.forEach(result::addFunction) + modelFunctions.filter { it !in used }.forEach { result.addFunction(function(it, result, null)) } + + val functionsByName = buildMap { result.functions.forEach { putIfAbsent(it.fullName, it) } } + val modelTypes = model.namedTypes.associateBy { it.name } + original.namedTypes.forEach { target -> + result.addNamedType(modelTypes[target.name]?.let { namedType(target, it, result, functionsByName) } ?: target) + } + model.namedTypes.filter { original.findNamedType(it.name) == null }.forEach { + result.addNamedType(namedType(null, it, result, functionsByName)) + } + original.globals.forEach(result::addGlobal) + model.globals.filter { result.findGlobal(it.name) == null }.forEach { result.addGlobal(global(it, result)) } + original.constants.forEach(result::addConst) + model.constants.filter { result.findConstant(it.name) == null }.forEach { result.addConst(constant(it, result)) } + result._imports += original.imports + result._imports += model.imports.mapNotNull { modelDependencies[it.importPath] } + .filter { dependency -> result._imports.none { it.importPath == dependency.importPath } } + result.initFunction = original.initFunction + return result + } + + private fun global(model: GoIRGlobal, pkg: GoIRPackage) = + GoIRGlobalImpl(model.name, symbol(model.fullName), type(model.type), pkg, model.isExported, model.position) + + private fun constant(model: GoIRConst, pkg: GoIRPackage) = GoIRConstImpl( + model.name, symbol(model.fullName), type(model.type), model.value, pkg, model.isExported, model.position, + ) + + private fun namedType( + original: GoIRNamedType?, + model: GoIRNamedType, + pkg: GoIRPackage, + functionsByName: Map, + ): GoIRNamedType { + val originalFields = original?.fields.orEmpty() + val originalNames = originalFields.mapTo(mutableSetOf()) { it.name } + val fields = originalFields + model.fields.filter { it.name !in originalNames } + .mapIndexed { index, field -> field.copy(type = type(field.type), index = originalFields.size + index) } + val underlying = when (val declared = original?.underlying ?: type(model.underlying)) { + is GoIRStructType -> GoIRStructType( + fields.map { GoIRStructField(it.name, it.type, it.isEmbedded, it.tag) }, + declared.structTypeRef, + ) + else -> declared + } + val result = GoIRNamedTypeImpl( + model.name, symbol(model.fullName), pkg, underlying, + original?.kind ?: model.kind, + original?.position ?: model.position, + original?.typeParams ?: model.typeParams.map { it.copy(constraint = type(it.constraint)) }, + ) + fields.mapTo(result._fields) { it.copy(enclosingType = result) } + result._methods += methods(original?.methods.orEmpty(), model.methods, functionsByName) + result._pointerMethods += methods(original?.pointerMethods.orEmpty(), model.pointerMethods, functionsByName) + result._interfaceMethods += original?.interfaceMethods ?: model.interfaceMethods.map { + it.copy(signature = type(it.signature) as GoIRFuncType, enclosingInterface = result) + } + result._embeddedInterfaces += original?.embeddedInterfaces ?: model.embeddedInterfaces.map { embedded -> + pkg.findNamedType(embedded.name)?.takeIf { path(embedded.pkg.importPath) == pkg.importPath } ?: embedded + } + return result + } + + private fun methods( + targetMethods: List, + modelMethods: List, + functionsByName: Map, + ): List { + val targetNames = targetMethods.mapTo(mutableSetOf()) { it.fullName } + return targetMethods.map { functionsByName[it.fullName] ?: it } + + modelMethods.map { symbol(it.fullName) }.filter { it !in targetNames }.mapNotNull(functionsByName::get) + } + + private fun sameSignature(model: GoIRFunction, target: GoIRFunction): Boolean = + sameType(type(model.signature), target.signature) && + model.isMethod == target.isMethod && + model.isPointerReceiver == target.isPointerReceiver + } + + private fun sameType(first: GoIRType, second: GoIRType): Boolean = + sameType(first, second, mutableListOf()) + + private fun sameType( + first: GoIRType, + second: GoIRType, + active: MutableList>, + ): Boolean { + if (first === second) return true + if (active.any { (left, right) -> left === first && right === second }) return true + active += first to second + return when { + first is GoIRBasicType && second is GoIRBasicType -> first.kind == second.kind + first is GoIRPointerType && second is GoIRPointerType -> + sameType(first.elem, second.elem, active) + first is GoIRArrayType && second is GoIRArrayType -> + first.length == second.length && sameType(first.elem, second.elem, active) + first is GoIRSliceType && second is GoIRSliceType -> + sameType(first.elem, second.elem, active) + first is GoIRMapType && second is GoIRMapType -> + sameType(first.key, second.key, active) && sameType(first.value, second.value, active) + first is GoIRChanType && second is GoIRChanType -> + first.direction == second.direction && sameType(first.elem, second.elem, active) + first is GoIRStructType && second is GoIRStructType -> + first.structTypeRef?.fullRefName == second.structTypeRef?.fullRefName && + first.fields.size == second.fields.size && + first.fields.zip(second.fields).all { (left, right) -> + left.name == right.name && left.isEmbedded == right.isEmbedded && + left.tag == right.tag && sameType(left.type, right.type, active) + } + first is GoIRNamedTypeRef && second is GoIRNamedTypeRef -> + first.typeRef.fullRefName == second.typeRef.fullRefName && + sameTypes(first.typeArgs, second.typeArgs, active) + first is GoIRFuncType && second is GoIRFuncType -> + first.isVariadic == second.isVariadic && + nullableTypeEqual(first.recv, second.recv, active) && + sameTypes(first.params, second.params, active) && + sameTypes(first.results, second.results, active) + first is GoIRTupleType && second is GoIRTupleType -> + sameTypes(first.elements, second.elements, active) + first is GoIRTypeParamType && second is GoIRTypeParamType -> + first.paramIndex == second.paramIndex && first.name == second.name && + sameType(first.constraint, second.constraint, active) + first is GoIRTypeParamRef && second is GoIRTypeParamRef -> + first.paramIndex == second.paramIndex && first.name == second.name + first is GoIRAnonymousInterfaceTypeRef && second is GoIRAnonymousInterfaceTypeRef -> + sameAnonymousInterface(first.interfaceType, second.interfaceType, active) + first is GoIRAnonymousInterfaceType && second is GoIRAnonymousInterfaceType -> + sameAnonymousInterface(first, second, active) + first is GoIRNamedInterfaceType && second is GoIRNamedInterfaceType -> + first.interfaceTypeRef.fullRefName == second.interfaceTypeRef.fullRefName + else -> false + } + } + + private fun sameAnonymousInterface( + first: GoIRAnonymousInterfaceType, + second: GoIRAnonymousInterfaceType, + active: MutableList>, + ): Boolean = first.methods.size == second.methods.size && + first.methods.zip(second.methods).all { (left, right) -> + left.name == right.name && sameType(left.signature, right.signature, active) + } && sameTypes(first.embeds, second.embeds, active) + + private fun sameTypes( + first: List, + second: List, + active: MutableList>, + ): Boolean = first.size == second.size && + first.zip(second).all { (left, right) -> sameType(left, right, active) } + + private fun nullableTypeEqual( + first: GoIRType?, + second: GoIRType?, + active: MutableList>, + ): Boolean = when { + first == null -> second == null + second == null -> false + else -> sameType(first, second, active) + } + + private fun initializerFunctions(pkg: GoIRPackage): Set { + val result = pkg.functions + .filterTo(mutableSetOf()) { it.name == "init" || it.name.startsWith("init#") } + do { + val oldSize = result.size + pkg.functions.forEach { function -> + val parentName = function.parent?.fullName + if (parentName != null && result.any { it.fullName == parentName }) { + result += function + } + } + } while (result.size != oldSize) + return result + } +} + + +internal fun modelFunction( + original: GoIRFunction?, + name: String, + fullName: String, + pkg: GoIRPackage?, + signature: GoIRFuncType, + params: List, + freeVars: List, + position: org.opentaint.ir.go.api.GoIRPosition?, + isMethod: Boolean, + isPointerReceiver: Boolean, + isExported: Boolean, + syntheticKind: String?, + hasBody: Boolean, + parent: GoIrFunctionReference?, + anonymousFunctions: List, + typeParams: List, + originFullName: String?, + typeArgs: List, +): GoIRFunctionImpl = if (original == null) { + GoIRModelFunctionImpl( + name, fullName, pkg, signature, params, freeVars, position, isMethod, isPointerReceiver, isExported, + syntheticKind, hasBody, parent, anonymousFunctions, typeParams, originFullName, typeArgs, + ) +} else { + GoIRApproximatedFunctionImpl( + original, name, fullName, pkg, signature, params, freeVars, position, isMethod, isPointerReceiver, + isExported, syntheticKind, hasBody, parent, anonymousFunctions, typeParams, originFullName, typeArgs, + ) +} + +private const val GO_MODEL_PREFIX = "opentaint/" +private const val UNIVERSE_PACKAGE_PATH = "" diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt deleted file mode 100644 index fd6d15d3c5..0000000000 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/client/GoIRTransformerChain.kt +++ /dev/null @@ -1,236 +0,0 @@ -package org.opentaint.ir.go.client - -import org.opentaint.ir.go.api.* -import org.opentaint.ir.go.impl.GoIRConstImpl -import org.opentaint.ir.go.impl.GoIRGlobalImpl -import org.opentaint.ir.go.impl.functionEquals -import org.opentaint.ir.go.impl.functionHashCode -import org.opentaint.ir.go.type.* - -/** Lookup view. Only finite package members and individual lookup results retain resolved values. */ -class GoIRTransformerChain( - private val source: GoIRProgram, - private val transformers: List, -) : GoIRProgram { - override val originalProgram: GoIRProgram get() = source.originalProgram - private val types = LookupTypes(this, source.anonymousInterfaces, ::path) - private fun path(value: String) = transformers.fold(value) { v, transformer -> transformer.packagePath(v) } - private fun symbol(value: String) = transformers.fold(value) { v, transformer -> transformer.symbolName(v) } - private val additionalPackages by lazy { - transformers.flatMap { it.additionalPackages }.associateBy { it.importPath } - } - override val lookup: GoIRLookup = GoIRProgramLookup(this) - - override val packages: Map by lazy { - (source.packages + additionalPackages).entries - .sortedBy { if (path(it.key) == it.key) 0 else 1 } - .associate { (key, original) -> - val resolvedPath = path(key) - resolvedPath to lazy { - fun resolve(index: Int): GoIRPackage? = if (index < 0) original else - transformers[index].resolvePackage(resolvedPath) { resolve(index - 1) } - PackageView(resolve(transformers.lastIndex) ?: original) - } - }.let { entries -> - object : AbstractMap() { - override fun get(key: String): GoIRPackage? = entries[key]?.value - override val entries: Set> - get() = entries.mapValues { it.value.value }.entries - } - } - } - override val anonymousInterfaces: Map - get() = types.anonymousInterfaces - - override fun findPackage(importPath: String): GoIRPackage? = packages[importPath] - - override fun allFunctions(): List = packages.values.flatMap { it.functions } - override fun allNamedTypes(): List = packages.values.flatMap { it.namedTypes } - override fun mainPackage(): GoIRPackage? = packages.values.find { it.name == "main" } - - private fun function(source: GoIRFunction): GoIRFunction { - val reference = reference(source.reference()) - fun resolve(index: Int): GoIRFunction? = if (index < 0) source else - transformers[index].resolveFunction(reference) { resolve(index - 1) } - return when (val resolved = resolve(transformers.lastIndex) ?: source) { - is GoIRApproximatedFunction -> ApproximationView(resolved) - is GoIRModelFunction -> ModelFunctionView(resolved) - else -> FunctionView(resolved, (resolved as? GoIRBodyReader) ?: (source as? GoIRBodyReader)) - } - } - - private fun reference(ref: GoIrFunctionReference): GoIrFunctionReference = - ref.copy(pkg = path(ref.pkg), signature = types(ref.signature) as GoIRFuncType).also { it.program = this } - - private open inner class FunctionView( - protected val source: GoIRFunction, - private val reader: GoIRBodyReader? = source as? GoIRBodyReader, - ) : GoIRFunction by source, GoIRBodyReader { - override val fullName get() = symbol(source.fullName) - override val originFullName get() = source.originFullName?.let(::symbol) - override val pkg: GoIRPackage? get() = source.pkg?.importPath?.let { findPackage(path(it)) } - override val signature by lazy { types(source.signature) as GoIRFuncType } - override val params by lazy { source.params.map { it.copy(type = types(it.type)) } } - override val freeVars by lazy { source.freeVars.map { it.copy(type = types(it.type)) } } - override val typeParams by lazy { source.typeParams.map { it.copy(constraint = types(it.constraint)) } } - override val typeArgs by lazy { source.typeArgs.map(types::invoke) } - override val receiverType get() = signature.recv - override val parent by lazy { source.parent?.let(::reference) } - override val anonymousFunctions by lazy { source.anonymousFunctions.map(::reference) } - override fun readBody(owner: GoIRFunction, context: GoIRBodyContext): GoIRBody? { - val mapped = object : GoIRBodyContext() { - override fun type(type: GoIRType) = context.type(types(type)) - override fun function(reference: GoIrFunctionReference) = context.function(reference(reference)) - override fun global(reference: GoIRGlobalReference) = context.global( - reference.copy(pkg = path(reference.pkg)).also { it.program = this@GoIRTransformerChain }, - ) - override fun fieldIndex(type: GoIRType, index: Int, name: String): Int { - val mappedType = types(type) - fun named(type: GoIRType): GoIRNamedType? = when (type) { - is GoIRPointerType -> named(type.elem) - is GoIRNamedTypeRef -> type.namedType - is GoIRStructType -> type.namedType - else -> null - } - val resolved = named(mappedType)?.fields?.firstOrNull { it.name == name }?.index ?: index - return context.fieldIndex(mappedType, resolved, name) - } - } - return reader?.readBody(owner, mapped) ?: source.body - } - override val body by lazy { readBody(this, GoIRBodyContext()) } - override val hasBody get() = source.hasBody - override val bodyAvailable get() = source.bodyAvailable - override val parameters get() = if (freeVars.isEmpty()) params - else params + freeVars.map { GoIRParameter(it.name, it.type, params.size + it.index) } - override val returnType get() = signature.results.first() - override fun flowGraph() = body?.instGraph ?: error("Function $name has no body") - override fun equals(other: Any?): Boolean = functionEquals(other) - override fun hashCode(): Int = functionHashCode() - override fun toString(): String = source.toString() - } - - private inner class ModelFunctionView(source: GoIRModelFunction) : FunctionView(source), GoIRModelFunction - private inner class ApproximationView(source: GoIRApproximatedFunction) : - FunctionView(source), GoIRApproximatedFunction { - override val original: GoIRFunction = source.original - } - - private inner class PackageView(val source: GoIRPackage) : GoIRPackage by source { - override val importPath get() = path(source.importPath) - override val functions by lazy { source.functions.map(::function) } - override val namedTypes by lazy { source.namedTypes.map { NamedTypeView(it, this) } } - override val globals: List by lazy { - source.globals.map { - GoIRGlobalImpl(it.name, symbol(it.fullName), types(it.type), this, it.isExported, it.position) - } - } - override val constants: List by lazy { - source.constants.map { - GoIRConstImpl(it.name, symbol(it.fullName), types(it.type), it.value, this, it.isExported, it.position) - } - } - override fun findGlobal(name: String) = globals.find { it.name == name } - override fun findConstant(name: String) = constants.find { it.name == name } - override val imports by lazy { source.imports.map { findPackage(path(it.importPath)) ?: it } } - override val initFunction by lazy { source.initFunction?.let { reference(it.reference()).function } } - override fun findNamedType(name: String) = namedTypes.find { it.name == name } - override fun allMethods() = namedTypes.flatMap { it.allMethods() } - override fun equals(other: Any?): Boolean = other is GoIRPackage && importPath == other.importPath - override fun hashCode(): Int = importPath.hashCode() - override fun toString(): String = "GoIRPackage($importPath)" - } - - private inner class NamedTypeView(private val source: GoIRNamedType, override val pkg: GoIRPackage) : - GoIRNamedType by source { - override val fullName get() = symbol(source.fullName) - override val underlying by lazy { types(source.underlying) } - override val fields by lazy { source.fields.map { it.copy(type = types(it.type), enclosingType = this) } } - override val methods by lazy { source.methods.map { reference(it.reference()).function } } - override val pointerMethods by lazy { source.pointerMethods.map { reference(it.reference()).function } } - override val typeParams by lazy { source.typeParams.map { it.copy(constraint = types(it.constraint)) } } - override val interfaceMethods by lazy { - source.interfaceMethods.map { it.copy(signature = types(it.signature) as GoIRFuncType, enclosingInterface = this) } - } - override val embeddedInterfaces by lazy { - source.embeddedInterfaces.map { findPackage(path(it.pkg.importPath))?.findNamedType(it.name) ?: it } - } - override fun allMethods() = methods + pointerMethods - override fun methodByName(name: String) = allMethods().find { it.name == name } - override fun equals(other: Any?): Boolean = other is GoIRNamedType && fullName == other.fullName - override fun hashCode(): Int = fullName.hashCode() - override fun toString(): String = "GoIRNamedType($fullName)" - } -} - -private class LookupTypes( - private val program: GoIRProgram, - originalInterfaces: Map, - private val path: (String) -> String, -) : (GoIRType) -> GoIRType { - private val lock = Any() - - // This namespace registry holds interface definitions from the finite input programs. - // It does not memoize transformed type graphs. IDs are local to each wire program, - // so model definitions must be interned before entering the shared lookup namespace. - private val interfaces = originalInterfaces.toMutableMap() - private val interfaceIds = mutableMapOf().apply { - originalInterfaces.forEach { (id, type) -> putIfAbsent(type, id) } - } - private var nextInterfaceId = (originalInterfaces.keys.maxOrNull() ?: -1) + 1 - - val anonymousInterfaces: Map = - object : AbstractMap() { - override fun get(key: Int): GoIRAnonymousInterfaceType? = synchronized(lock) { - interfaces[key]?.let { invoke(it) as GoIRAnonymousInterfaceType } - } - override val entries: Set> - get() = synchronized(lock) { - interfaces.toMap().mapValues { invoke(it.value) as GoIRAnonymousInterfaceType }.entries - } - } - - private fun interfaceId(type: GoIRAnonymousInterfaceType): Int = interfaceIds.getOrPut(type) { - nextInterfaceId++.also { interfaces[it] = type } - } - - private fun reference(ref: NamedTypeRef) = ref.copy(refTypePkg = path(ref.refTypePkg)).also { it.program = program } - - override fun invoke(type: GoIRType): GoIRType = synchronized(lock) { - val result = when (type) { - is GoIRTypeParamType -> type.copy(constraint = invoke(type.constraint)) - is GoIRTypeParamRef -> type - is GoIRPointerType -> GoIRPointerType(invoke(type.elem)) - is GoIRArrayType -> GoIRArrayType(invoke(type.elem), type.length) - is GoIRSliceType -> GoIRSliceType(invoke(type.elem)) - is GoIRMapType -> GoIRMapType(invoke(type.key), invoke(type.value)) - is GoIRChanType -> GoIRChanType(invoke(type.elem), type.direction) - is GoIRStructType -> GoIRStructType( - type.fields.map { GoIRStructField(it.name, invoke(it.type), it.isEmbedded, it.tag) }, - type.structTypeRef?.let(::reference), - ) - is GoIRNamedTypeRef -> GoIRNamedTypeRef(reference(type.typeRef), type.typeArgs.map(::invoke)) - is GoIRNamedInterfaceType -> GoIRNamedInterfaceType( - type.methods.map { GoIRInterfaceMethodSig(it.name, invoke(it.signature) as GoIRFuncType) }, - type.embeds.map(::invoke), - reference(type.interfaceTypeRef), - ) - is GoIRAnonymousInterfaceTypeRef -> GoIRAnonymousInterfaceTypeRef(interfaceId(type.interfaceType)) - .also { it.program = program } - is GoIRAnonymousInterfaceType -> GoIRAnonymousInterfaceType( - interfaceId(type), - type.methods.map { GoIRInterfaceMethodSig(it.name, invoke(it.signature) as GoIRFuncType) }, - type.embeds.map(::invoke), - ) - is GoIRFuncType -> GoIRFuncType( - type.params.map(::invoke), - type.results.map(::invoke), - type.isVariadic, - type.recv?.let(::invoke), - ) - is GoIRTupleType -> GoIRTupleType(type.elements.map(::invoke)) - else -> type - } - result - } -} diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt index c76af95e82..80012bea0d 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRFunctionImpl.kt @@ -1,6 +1,5 @@ package org.opentaint.ir.go.impl -import org.opentaint.ir.go.client.GoIRBodyReader import org.opentaint.ir.go.client.GoIRBodyContext import org.opentaint.ir.go.api.GoIRBody import org.opentaint.ir.go.api.GoIRBodyUnavailableException @@ -14,7 +13,7 @@ import org.opentaint.ir.go.api.GoIrFunctionReference import org.opentaint.ir.go.type.GoIRFuncType import org.opentaint.ir.go.type.GoIRType -class GoIRFunctionImpl( +open class GoIRFunctionImpl( override val name: String, override val fullName: String, override val pkg: GoIRPackage?, @@ -33,7 +32,7 @@ class GoIRFunctionImpl( override val typeParams: List = emptyList(), override val originFullName: String? = null, override val typeArgs: List = emptyList(), -) : GoIRFunction, GoIRBodyReader { +) : GoIRFunction { private var _body: GoIRBody? = null private var reader: ((GoIRFunction, GoIRBodyContext) -> GoIRBody)? = null @@ -42,7 +41,8 @@ class GoIRFunctionImpl( this.reader = reader } - override fun readBody(owner: GoIRFunction, context: GoIRBodyContext): GoIRBody? = + /** Decodes this body for [owner], resolving its symbols through [context]. */ + internal fun readBody(owner: GoIRFunction, context: GoIRBodyContext): GoIRBody? = reader?.invoke(owner, context) ?: _body private val resolvedBody by lazy { reader?.invoke(this, GoIRBodyContext()) } diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRModelFunctionImpl.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRModelFunctionImpl.kt new file mode 100644 index 0000000000..617ef4e064 --- /dev/null +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRModelFunctionImpl.kt @@ -0,0 +1,62 @@ +package org.opentaint.ir.go.impl + +import org.opentaint.ir.go.api.GoIRApproximatedFunction +import org.opentaint.ir.go.api.GoIRFreeVar +import org.opentaint.ir.go.api.GoIRFunction +import org.opentaint.ir.go.api.GoIRModelFunction +import org.opentaint.ir.go.api.GoIRPackage +import org.opentaint.ir.go.api.GoIRParameter +import org.opentaint.ir.go.api.GoIRPosition +import org.opentaint.ir.go.api.GoIRTypeParamDecl +import org.opentaint.ir.go.api.GoIrFunctionReference +import org.opentaint.ir.go.type.GoIRFuncType +import org.opentaint.ir.go.type.GoIRType + +open class GoIRModelFunctionImpl( + name: String, + fullName: String, + pkg: GoIRPackage?, + signature: GoIRFuncType, + params: List, + freeVars: List, + position: GoIRPosition?, + isMethod: Boolean, + isPointerReceiver: Boolean, + isExported: Boolean, + syntheticKind: String?, + declaredHasBody: Boolean, + parent: GoIrFunctionReference?, + anonymousFunctions: List, + typeParams: List, + originFullName: String?, + typeArgs: List, +) : GoIRFunctionImpl( + name, fullName, pkg, signature, params, freeVars, position, isMethod, isPointerReceiver, isExported, + isSynthetic = true, syntheticKind, declaredHasBody, parent, anonymousFunctions, typeParams, originFullName, typeArgs, +), GoIRModelFunction { + override fun toString(): String = "GoIRModelFunction($fullName)" +} + +class GoIRApproximatedFunctionImpl( + override val original: GoIRFunction, + name: String, + fullName: String, + pkg: GoIRPackage?, + signature: GoIRFuncType, + params: List, + freeVars: List, + position: GoIRPosition?, + isMethod: Boolean, + isPointerReceiver: Boolean, + isExported: Boolean, + syntheticKind: String?, + declaredHasBody: Boolean, + parent: GoIrFunctionReference?, + anonymousFunctions: List, + typeParams: List, + originFullName: String?, + typeArgs: List, +) : GoIRModelFunctionImpl( + name, fullName, pkg, signature, params, freeVars, position, isMethod, isPointerReceiver, isExported, + syntheticKind, declaredHasBody, parent, anonymousFunctions, typeParams, originFullName, typeArgs, +), GoIRApproximatedFunction diff --git a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRProgramImpl.kt b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRProgramImpl.kt index 36b45b5921..1711fa63e7 100644 --- a/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRProgramImpl.kt +++ b/core/opentaint-ir/go/go-ir-client/src/main/kotlin/org/opentaint/ir/go/impl/GoIRProgramImpl.kt @@ -4,9 +4,53 @@ import org.opentaint.ir.go.api.* import org.opentaint.ir.go.type.GoIRAnonymousInterfaceType class GoIRProgramImpl( - override val packages: Map, - override val anonymousInterfaces: Map, + private val declaredPackages: Map, + private val declaredInterfaces: MutableMap, ) : GoIRProgram { + private var transformers: List = emptyList() + + private val resolvedPackages: Lazy>> = lazy { + val additional = transformers.flatMap { it.additionalPackages }.associateBy { it.importPath } + (declaredPackages.keys + additional.keys).associateWith { importPath -> + lazy { + fun resolve(index: Int): GoIRPackage? = if (index < 0) { + declaredPackages[importPath] ?: additional[importPath] + } else { + transformers[index].resolvePackage(importPath) { resolve(index - 1) } + } + resolve(transformers.lastIndex) + } + } + } + + /** Installs package lookup interceptors. Must be called before the first package lookup. */ + fun install(transformers: List) { + check(!resolvedPackages.isInitialized()) { "Go IR transformers must be installed before package lookup" } + this.transformers = transformers + } + + /** The package as it was deserialized, before any transformer. */ + internal fun declaredPackage(importPath: String): GoIRPackage? = declaredPackages[importPath] + + internal fun addAnonymousInterface(type: GoIRAnonymousInterfaceType) { + check(declaredInterfaces.putIfAbsent(type.id, type) == null) { "Anonymous interface ${type.id} already exists" } + } + + override val anonymousInterfaces: Map get() = declaredInterfaces + + override val packages: Map by lazy { + if (transformers.isEmpty()) return@lazy declaredPackages + object : AbstractMap() { + override fun get(key: String): GoIRPackage? = resolvedPackages.value[key]?.value + override fun containsKey(key: String): Boolean = get(key) != null + override val entries: Set> by lazy { + resolvedPackages.value.entries + .mapNotNull { (importPath, pkg) -> pkg.value?.let { java.util.AbstractMap.SimpleEntry(importPath, it) } } + .toSet() + } + } + } + override fun findPackage(importPath: String) = packages[importPath] override fun allFunctions(): List = diff --git a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt index 615340c145..a9c4337a6e 100644 --- a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt +++ b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt @@ -7,9 +7,10 @@ import org.junit.jupiter.api.extension.ExtendWith import org.opentaint.ir.go.api.GoIRApproximatedFunction import org.opentaint.ir.go.api.GoIRModelFunction import org.opentaint.ir.go.api.GoIRFunction -import org.opentaint.ir.go.api.reference +import org.opentaint.ir.go.api.GoIRPackage import org.opentaint.ir.go.api.GoIRTransformer -import org.opentaint.ir.go.client.GoIRTransformerChain +import org.opentaint.ir.go.impl.GoIRPackageImpl +import org.opentaint.ir.go.impl.GoIRProgramImpl import org.opentaint.ir.go.cfg.GoIRCallTarget import org.opentaint.ir.go.client.GoIRLoadConfig import org.opentaint.ir.go.client.GoIRLoadMode @@ -88,11 +89,9 @@ class ModelTests { assertThat(closureTarget.function).isSameAs(function) assertThat(function.body!!.function).isSameAs(function) assertThat(caller.body!!.instructions).allMatch { it.location.method === caller } - val originalCaller = program.originalProgram.findPackage(pkg.importPath)!!.functions.single { it.name == "Upper" } - val rawTarget = originalCaller.findInstructions().single().call.target as GoIRCallTarget.Function - assertThat(rawTarget.function).isSameAs((function as GoIRApproximatedFunction).original) - assertThat(rawTarget.function).isNotSameAs(function) - + val original = (function as GoIRApproximatedFunction).original + assertThat(original).isNotSameAs(function) + assertThat(original.fullName).isEqualTo(function.fullName) } @Test @@ -112,14 +111,13 @@ class ModelTests { val effectiveProgram = builder.buildFromDir(project, modelConfig(model)) val effectivePackage = effectiveProgram.findPackage("example.com/original")!! val effectiveIdentity = effectivePackage.functions.single { it.name == "Identity" } - val originalProgram = effectiveProgram.originalProgram - val originalPackage = originalProgram.findPackage("example.com/original")!! - val originalIdentity = originalPackage.functions.single { it.name == "Identity" } + assertThat(effectiveIdentity).isInstanceOf(GoIRApproximatedFunction::class.java) + val originalIdentity = (effectiveIdentity as GoIRApproximatedFunction).original + val originalPackage = originalIdentity.pkg!! - assertThat(originalProgram).isNotSameAs(effectiveProgram) + assertThat(originalPackage).isNotSameAs(effectivePackage) assertThat(effectiveIdentity.pkg).isSameAs(effectivePackage) - assertThat(effectiveIdentity).isInstanceOf(GoIRApproximatedFunction::class.java) - assertThat((effectiveIdentity as GoIRApproximatedFunction).original).isSameAs(originalIdentity) + assertThat(originalPackage.functions.single { it.name == "Identity" }).isSameAs(originalIdentity) assertThat(effectiveIdentity.findInstructions()) .anyMatch { it.call.target?.displayName == "example.com/original.helper" } assertThat(originalIdentity.isSynthetic).isFalse() @@ -130,7 +128,7 @@ class ModelTests { } @Test - fun `model views keep identity consistent with the original Go IR`(builder: GoIRTestBuilder) { + fun `model packages keep identity consistent with the original Go IR`(builder: GoIRTestBuilder) { val project = project( "example.com/original", "package original\n" + @@ -149,7 +147,7 @@ class ModelTests { val effectiveProgram = builder.buildFromDir(project, modelConfig(model)) val effective = effectiveProgram.findPackage("example.com/original")!! - val original = effectiveProgram.originalProgram.findPackage("example.com/original")!! + val original = (effective.functions.single { it.name == "Identity" } as GoIRApproximatedFunction).original.pkg!! fun assertSameIdentity(first: Any, second: Any) { assertThat(first).isEqualTo(second) assertThat(second).isEqualTo(first) @@ -221,7 +219,7 @@ class ModelTests { val receiverType = (put.params.first().type as GoIRPointerType).elem as GoIRNamedTypeRef assertThat(receiverType.namedType).isSameAs(box) assertThat(receiverType.namedType.pointerMethods.single { it.name == "Put" }).isSameAs(put) - assertThat(program.originalProgram.findPackage(targetPackage.importPath)!!.findNamedType("Box")!!.fields.map { it.name }) + assertThat((put as GoIRApproximatedFunction).original.pkg!!.findNamedType("Box")!!.fields.map { it.name }) .containsExactly("Original") val shadowAccess = put.findExpressions().single { it.fieldName == "Shadow" } assertThat(shadowAccess.fieldIndex).isEqualTo(1) @@ -643,7 +641,8 @@ class ModelTests { assertThat(program.findPackage("example.com/unrelated")).isNotNull() assertThat(program.findPackage("example.com/absent")).isNull() assertThat(program.findPackage("net/url")).isNull() - assertThat(program.packages.keys).containsExactlyInAnyOrderElementsOf(program.originalProgram.packages.keys) + val withoutModels = builder.buildFromDir(project, GoIRLoadConfig(mode = GoIRLoadMode.PROJECT)) + assertThat(program.packages.keys).containsExactlyInAnyOrderElementsOf(withoutModels.packages.keys) } @Test @@ -687,7 +686,7 @@ class ModelTests { .functions.single { it.name == "Identity" && it.parent == null } val call = identity.findInstructions().single() - assertThat(program.originalProgram.findPackage("net/url")).isNull() + assertThat(builder.buildFromDir(project, GoIRLoadConfig(mode = GoIRLoadMode.PROJECT)).findPackage("net/url")).isNull() assertThat(program.findPackage("net/url")).isNotNull() assertThat(call.call.target?.displayName).isEqualTo("net/url.QueryEscape") } @@ -840,82 +839,26 @@ class ModelTests { } @Test - fun `transformers compose in order and resolve calls through one cached view`(builder: GoIRTestBuilder) { - val original = builder.buildFromSource("package p\nfunc Target() {}\nfunc Caller() { Target() }\n") - fun suffix(value: String) = object : GoIRTransformer { - override fun resolveFunction( - reference: org.opentaint.ir.go.api.GoIrFunctionReference, - next: () -> GoIRFunction?, - ): GoIRFunction? { - val function = next() ?: return null - return object : GoIRFunction by function { - override val syntheticKind = function.syntheticKind.orEmpty() + value - } - } - } - val transformed = GoIRTransformerChain(original, listOf(suffix("first"), suffix("second"))) - val pkg = transformed.packages.getValue("test/p") - val target = pkg.functions.single { it.name == "Target" } - val caller = pkg.functions.single { it.name == "Caller" } - assertThat(target.syntheticKind).isEqualTo("firstsecond") - val call = caller.findInstructions().single().call.target as GoIRCallTarget.Function - assertThat(call.function).isSameAs(target) - assertThat(transformed.allFunctions()).contains(target, caller) - assertThat(original.findPackage("test/p")!!.functions.single { it.name == "Target" }.syntheticKind).isNull() - } - - @Test - fun `lookup interceptor can bypass later resolvers and leaves bodies unread`(builder: GoIRTestBuilder) { - val original = builder.buildFromSource("package p\nfunc Target() {}\nfunc Caller() { Target() }\n") - val target = original.findPackage("test/p")!!.functions.single { it.name == "Target" } - var ownedFallbacks = 0 - var bodyReads = 0 - val fallback = object : GoIRTransformer { - override fun resolveFunction( - reference: org.opentaint.ir.go.api.GoIrFunctionReference, - next: () -> GoIRFunction?, - ): GoIRFunction? { - if (reference.name == "Target") ownedFallbacks++ + fun `package transformers intercept lookup in order and can bypass earlier resolvers`(builder: GoIRTestBuilder) { + val program = builder.buildFromSource("package p\nfunc Target() {}\nfunc Caller() { Target() }\n") as GoIRProgramImpl + val resolved = mutableListOf() + fun recording(name: String) = object : GoIRTransformer { + override fun resolvePackage(importPath: String, next: () -> GoIRPackage?): GoIRPackage? { + if (importPath == "test/p") resolved += name return next() } } val interceptor = object : GoIRTransformer { - override fun resolveFunction( - reference: org.opentaint.ir.go.api.GoIrFunctionReference, - next: () -> GoIRFunction?, - ): GoIRFunction? { - if (reference.name != "Target") return next() - return object : GoIRFunction by target { - override val syntheticKind = "intercepted" - override val body get() = target.body.also { bodyReads++ } - } + override fun resolvePackage(importPath: String, next: () -> GoIRPackage?): GoIRPackage? { + return if (importPath == "test/p") GoIRPackageImpl(importPath, "p") else next() } } - val program = GoIRTransformerChain(original, listOf(fallback, interceptor)) - val pkg = program.findPackage("test/p")!! - val resolved = pkg.functions.single { it.name == "Target" } - val call = pkg.functions.single { it.name == "Caller" } - .findInstructions().single().call.target as GoIRCallTarget.Function - assertThat(resolved.syntheticKind).isEqualTo("intercepted") - assertThat(call.lookup.pkg).isEqualTo("test/p") - assertThat(call.function).isSameAs(resolved) - assertThat(call.function).isSameAs(resolved) - assertThat(ownedFallbacks).isZero() - assertThat(bodyReads).isZero() - } + program.install(listOf(recording("first"), interceptor, recording("last"))) - @Test - fun `method and initializer lookup reuse package members`(builder: GoIRTestBuilder) { - val original = builder.buildFromSource( - "package p\ntype Box struct {}\nfunc (b *Box) Method() {}\nfunc init() {}\n", - ) - val program = GoIRTransformerChain(original, emptyList()) val pkg = program.findPackage("test/p")!! - val method = pkg.functions.single { it.name == "Method" } - assertThat(pkg.findNamedType("Box")!!.pointerMethods.single()).isSameAs(method) - val call = GoIRCallTarget.Function(method.reference().also { it.program = program }) - assertThat(call.displayName).isEqualTo(method.fullName) - assertThat(pkg.initFunction).isSameAs(pkg.functions.single { it.name == "init" }) + assertThat(pkg.functions).isEmpty() + assertThat(program.findPackage("test/p")).isSameAs(pkg) + assertThat(resolved).containsExactly("last") } private fun project(module: String, source: String): Path {